React Native #4 UI 개발하기
React의 지난 16, 17, 18 버전의 주요 업데이트 내역 살펴보기
2024-10-01
Explanation
오늘은, 간단하게나마 React의 지난 16, 17, 18 버전의 업데이트 내역을 살펴보려 합니다.
글에 사용되는 코드는 깃허브 저장소에서 확인하실 수 있습니다.
https://github.com/falsy/blog-post-example/tree/main/basic-react
16 버전부터 시작합니다!
리액트 16.0 버전의 변경사항은 크게 fragments, errorBoundary, portals, custom DOM 속성 지원과 파일 크기 및 server-side 렌더링 개선입니다.
https://legacy.reactjs.org/blog/2017/09/26/react-v16.0.html
[fragments]
그”<Fragment> … </Fragment>” 또는 “<> … </>” 이걸 이야기하는 건 아니고 아래와 같이 배열 요소를 렌더에 응답 값으로 사용할 수 있습니다.
1 2 3 4 5 6 7 8 9 |
render() { // No need to wrap list items in an extra element! return [ // Don't forget the keys :) <li key="A">First item</li>, <li key="B">Second item</li>, <li key="C">Third item</li>, ]; } |
[errorBoundary]
이전까지는 컴포넌트에서 오류가 발생하면 전체 애플리케이션이 중단되었는데, 이제 새롭게 추가된 “componentDidCatch(error, info)” 또는 “static getDerivedStateFromError()” (또는 둘 모두의) 라이프사이클 메서드를 사용해서 이름 그대로 오류의 경계를 만들 수 있습니다.
https://ko.legacy.reactjs.org/docs/error-boundaries.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다. return { hasError: true }; } componentDidCatch(error, errorInfo) { // 에러 리포팅 서비스에 에러를 기록할 수도 있습니다. logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 폴백 UI를 커스텀하여 렌더링할 수 있습니다. return <h1>Something went wrong.</h1>; } return this.props.children; } } |
1 2 3 |
<ErrorBoundary> <MyWidget /> </ErrorBoundary> |
알아둘 점은, 자식 요소에서의 에러만을 감지하고, 서버 사이드 렌더링, 이벤트 핸들러, 비동기적 코드(예: setTimeout 혹은 requestAnimationFram)는 감하지 않습니다.
[Portals]
Portal은 부모 컴포넌트의 DOM 계층 구조 밖의 노드로 자식을 렌더링합니다.
1 2 3 4 5 6 7 8 9 10 11 12 |
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>React</title> </head> <body> <div id="root"></div> <div id="modal-root"></div> </body> </html> |
이렇게 id 값이 “modal-root” 라는 엘리먼트를 만들어 놓고
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import { ReactNode, useState } from "react" import ReactDOM from "react-dom" const Modal = ({ children }: { children: ReactNode }) => { return ReactDOM.createPortal( <div className="modal">{children}</div>, document.getElementById("modal-root") as HTMLElement ) } export default function Portal() { const [isModalOpen, setModalOpen] = useState(false) const toggleModal = () => { setModalOpen(!isModalOpen) } return ( <div> <h1>React Portals</h1> <button onClick={toggleModal}> {isModalOpen ? "Close Modal" : "Open Modal"} </button> {isModalOpen && ( <Modal> <div> <h2>Modal</h2> <button onClick={toggleModal}>Close</button> </div> </Modal> )} </div> ) } |
이렇게 하면, Modal 컴포넌트는 “modal-root” 엘리먼트의 자식 요소로 생성된답니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
... <body> <div id="root"> ... </div> <div id="modal-root"> <div class="modal"> <div> <h2>Modal</h2> <button>Close</button> </div> </div> </div> </body> </html> |
생각보다.. 글이 길어지고 있어서.. 이제는 더 간단하게..
[custom DOM]
custom DOM은 이전 버전에는 비표준 HTML 속성을 모두 제거했는데 v16부터는 클라이언트와 서버 렌더러가 모든 비표준 속성을 허용한다는 이야기입니다.
[파일 크기 개선]
그 다음 파일 크기 개선은 말 그대로 파일의 크기가 30% 정도 줄었다고 해요.
[SSR(Server-Side Rendering) 개선]
마지막으로 SSR(Server-Side Rendering) 개선은 이전까지는 render 메서드를 사용했었는데, v16부터는 hydrate 메서드가 추가되었습니다. 만약 React를 서버 사이드에서 사용한다면
1 2 3 |
import { render } from "react-dom" import MyPage from "./MyPage" render(<MyPage/>, document.getElementById("content")); |
v16 이전까지 위와 같이 선언했다면,
1 2 3 |
import { hydrate } from "react-dom" import MyPage from "./MyPage" hydrate(<MyPage/>, document.getElementById("content")); |
v16 이후는 위와 같이 선언
서버 사이드 렌더링을 사용할 때, hydrate를 사용한다면, hydrate는 SSR로 이미 생성된 HTML을 받아서 그것과 연관된 React 컴포넌트를 연결(Mount)하는 작업을 수행합니다. 여기서 React는 이미 서버에서 생성된 HTML과 비교하면서 필요한 부분만 업데이트하고 이벤트 바인딩을 설정합니다.
이로써 서버에서 생선한 HTML을 재사용하기에 초기 로딩 속도가 빨라지고 이후 클라이언트에서 생성한 React 컴포넌트의 가상 DOM과 비교하여 필요한 부분만 리렌더링하기에 효율적으로 DOM을 업데이트 한답니다!
v16.2 에서 아까 잠깐 이야기 나왔던, DOM에 노드를 추가하지 않고 자식의 목록을 그룹화 할 수 있습니다.
1 2 3 4 5 6 7 8 9 |
render() { return ( <> <ChildA /> <ChildB /> <ChildC /> </> ); } |
또는
1 2 3 4 5 |
<React.Fragment> <ChildA /> <ChildB /> <ChildC /> </React.Fragment> |
– Official Context API
– createRef API
– forwardRef API
v16.3 에서 Context API가 공식적으로 추가되었습니다.
– React.memo
– React.lazy
[lazy]
v16.6 에서 lazy가 추가되었는데, 간단하게 살펴보면,
1 2 3 4 5 6 7 8 9 10 11 |
import React from "react" const LazyComponent: React.FC = () => { return ( <div> <p>This is a lazily loaded component</p> </div> ) } export default LazyComponent |
지연 로드할 컴포넌트를 만들고
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { lazy, Suspense, useState } from "react" import Portal from "./Portal" const LazyComponent = lazy(() => import("./LazyComponent")) export default function App() { const [load, setLoad] = useState(false) return ( <div> <h1>Basic React</h1> <section> <h2>Lazy</h2> <button onClick={() => setLoad(true)}>Load</button> {load && ( <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> )} </section> </div> ) } |
이렇게 상태에 따라 동적으로 컴포넌트로 로드하여 사용할 수 있습니다.
lazy를 잘 안 사용해서.. “Suspense”가 v18에서 추가된 줄 알았는데, 이제보니까 Suspense는 v16.6에서 lazy와 함께 추가되었고 v18에서 공식적으로 추가된 거였네요.. 민망쓰..
v16.8에서는 React의 Hooks 추가되었습니다.
– useState, useReducer
– useEffect, useMemo, useCallback
useReducer를 보면 Redux와 같은 구조로 동작을 하는데요, 물론 useReducer는 지역 상태를 관리하지만 페이스북이 만든 React와 React를 보안하기 위해 만든 Flux 아키텍처와 Flux의 아이디어로 개발된 Redux와 그 Redux를 참고하여 만든 useReducer라니 너무 멋지지 않나요?!
17 버전은 새로운 기능 도입 없이 내부 동작의 개선이나 안정성, 호환성을 개선하는 업데이트를 하였습니다.
– 점진적 업그레이드
– 이벤트 시스템 개선
– 새로운 JSX 트랜스폼 지원
– 일부 라이프사이클 메서드 제거
점진적 업그레이드가 가능하게 변경이 되었는데요. 이게 무슨말이냐면 이전까지는 새로운 React 버전이 릴리스되고 새로운 버전으로 업데이트를 하면 애플리케이션의 전체 코드를 업데이트 된 버전의 코드로 변경해 줘야했었는데 v17 부터는 부분적으로, 점진적으로 마이그레이션이 가능하게 되었습니다.
이전까지 이벤트가 Document에 위임했었는데 v17 부터는 React의 Root로 DOM 컨테이너에 이벤트를 연결합니다. 이전까지 Document에서 모든 이벤트를 제어하게 되면 외부의 라이브러리나 여러 Root를 사용할 때 문제가 발생할 여지가 있습니다. 그리고 Documnet라는 하나의 리스너에 모든 이벤트를 처리하는 것은 비표준적이기 때문에 Root DOM 컨테이너를 사용하는 것이 일반적인 브라우저 이벤트 처리 방식과 유사하게 동작합니다.
새로운 JSX 트랜스폼 지원으로 “import React from ‘react'” 구문을 사용하지 않아도 새로운 JSX 트랜스폼에 의해 자동으로 JSX를 변환하는 코드를 추가합니다.
componentWillMount, componentWillReceiveProps, componentWillUpdate 메서드가 제거되었습니다. 이는 렌더링이 취소되거나 중단될 때 의도하지 않은 부작용을 일으킬 수 있어서 제거되었습니다.
componentWillMount 대신 componentDidMount를 사용하여 컴포넌트아 마운트 된 후에 호출되도록 하고, componentWillReceiveProps 대신 getDerivedStateFromProps를 사용하여 새로운 props를 받아서 안전하게 업데이트하며 componentWillUpdate 대신 getSnapshotBeforeUpdate를 사용하여 DOM이 업데이트 되기전 스냅샷을 찍어 업데이트 후 이를 참조하도록 합니다.
v17.0.1, v17.0.2 에서는 대체로 버그 수정과 호환성 개선, 그리고 React DevTools 개선이 있었습니다.
마지막으로 글 작성 시점의 최신 버전인 v18 에 대해서 알아봅니다.
[createRoot]
이전의 render()를 사용하지 않고 createRoot를 사용하여야 v18에 추가된 새로운 동작을 수행합니다.
1 2 3 4 5 |
// v17 import ReactDOM from "react-dom" import App from "./App" ReactDOM.render(<App />, document.getElementById("root")) |
1 2 3 4 5 6 7 |
// v18 import { createRoot } from "react-dom/client" import App from "./App" const container = document.getElementById("root") const root = createRoot(container) root.render(<App />) |
기존의 render 메서드를 사용하던 방식이 createRoot로 변경된 것처럼, v16에서 SSR 개선을 위해 추가된 hydrate 메서드 역시 hydrateRoot로 지원합니다.
[Automatic Batching]
다음으로 “Automatic Batching” 기능이 추가되었습니다.
아래의 예를 보면,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { useState } from "react" export default function Batching() { const [state1, setState1] = useState(0) const [state2, setState2] = useState(false) const onClick = () => { setState1((prev) => prev + 1) setState2((prev) => !prev) } console.log("render") return ( <> <div>{state1}</div> <div>{state2.toString()}</div> <button onClick={onClick}>button</button> </> ) } |
위와 같이 클릭시 setState가 2번 일어나지만, 해당 컴포넌트의 렌더링은 1번 발생해서 “render”라는 로그는 1번만 출력된답니다. 이걸 일괄 처리(Batching)라고 하는데요.
사실, 위 일괄 처리(Batching)는 v18 이전에서도 동작하던 내용이고 새롭게 추가된 내용은 아래와 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import { useState } from "react" export default function Batching2() { const [state1, setState1] = useState(0) const [state2, setState2] = useState(false) const onClick = () => { setTimeout(() => { setState1((prev) => prev + 1) setState2((prev) => !prev) }, 500) } console.log("render") return ( <> <div>{state1}</div> <div>{state2.toString()}</div> <button onClick={onClick}>button</button> </> ) } |
v18 이전까지는 Promise, 네이티브 이벤트 핸들러, setTimeout 또는 기타 이벤트 내부의 업데이트는 기본적으로 일괄 처리 되지 않았지만 v18 부터는 자동으로 일괄 처리됩니다.
간단히 이야기하면 기존의 일괄 처리(Batching)가 콜스택 범위까지만 동작을 했고 v18 부터는 태스트 큐까지 동작하도록 업데이트 되었습니다.
[startTransition, useTransition]
다음으로 “startTransition” 및 “useTransition” 훅이 추가되었는데요.
참고: https://react.dev/reference/react/useTransition
아래의 예시 코드를 먼저 살펴보면,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import { useState } from "react" import PostsTab from "./SlowPost" export default function TransitionState() { const [menu, setMenu] = useState("A") const handleClick = (name: string) => { setMenu(name) } return ( <div style={{ maxHeight: "200px", overflow: "hidden" }}> <button onClick={() => handleClick("A")} style={{ color: menu === "A" ? "red" : "black" }} > A </button> <button onClick={() => handleClick("B")} style={{ color: menu === "B" ? "red" : "black" }} > B </button> <button onClick={() => handleClick("C")} style={{ color: menu === "C" ? "red" : "black" }} > C </button> <div style={{ padding: "5px 10px", background: "#f5f5f5" }}> {menu === "A" && <p>A</p>} {menu === "B" && <PostsTab />} {menu === "C" && <p>C</p>} </div> </div> ) } |
위 샘플 코드에서 “PostsTab” 컴포넌트가 렌더링 하는데 굉장히 느린 컴포넌트이고 이때 “A”, “B”, “C”를 연달아 클릭하면 B가 렌더링 하는 동안 UI가 멈추고 C가 뒤늦게 활성화되는 것을 확인할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import { useState, useTransition } from "react" import PostsTab from "./SlowPost" export default function Transition() { const [_, startTransition] = useTransition() const [menu, setMenu] = useState("A") const handleClick = (name: string) => { startTransition(() => { setMenu(name) }) } return ( <div style={{ maxHeight: "200px", overflow: "hidden" }}> <button onClick={() => handleClick("A")} style={{ color: menu === "A" ? "red" : "black" }} > A </button> <button onClick={() => handleClick("B")} style={{ color: menu === "B" ? "red" : "black" }} > B </button> <button onClick={() => handleClick("C")} style={{ color: menu === "C" ? "red" : "black" }} > C </button> <div style={{ padding: "5px 10px", background: "#f5f5f5" }}> {menu === "A" && <p>A</p>} {menu === "B" && <PostsTab />} {menu === "C" && <p>C</p>} </div> </div> ) } |
하지만 위와 같이 useTransition 훅을 사용하고 마찬가지로 “A”, “B”, “C”를 연달아 클릭한다면, “B”의 렌더링과 상관없이 바로 마지막 선택한 “C”가 활성화되는 것을 확인할 수 있습니다.
이와 같이 useTransition는 UI를 차단하지 않고 상태를 업데이트 할 수 있는 훅입니다. 그리고 사용하지 않은 _ 인자는 “isPending” 플래그로 대기중인 Transition이 있는지 알려줍니다. 예를 들면, isPending 플래그를 사용해서 Loading 뷰를 보여줄 수 있습니다.
[userId]
이어서 계속 추가된 훅을 알아보겠습니다. 다음은 “userId” 입니다.
참고: https://react.dev/reference/react/useId
1 2 3 4 5 6 7 8 9 10 |
<label> Password: <input type="password" aria-describedby="password-hint" /> </label> <p id="password-hint"> The password should contain at least 18 characters </p> |
React 에서는 컴포넌트는 반복적으로 사용될 수 있기 때문에 ID를 직접 입력하는 것을 추천하지 않습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { useId } from 'react' function PasswordField() { const passwordHintId = useId() return ( <> <label> Password: <input type="password" aria-describedby={passwordHintId} /> </label> <p id={passwordHintId}> The password should contain at least 18 characters </p> </> ) } |
위와 같이 “useId” 훅을 사용하면 화면에 여러 번 나타나도 ID가 출돌하지 않습니다.
리스트에서 key 값으로 “useId”를 사용하지 않습니다. 리스트에서 key는 데이터에서 생성합니다.
그밖의 추가된 훅은 아래와 같습니다.
– useDeferredValue : 급하지 않은 리렌더링을 지연시킬 수 있습니다.
– useSyncExternalStore (라이브러리에서 사용) : React 컴포넌트가 외부 스토어(상태 관리 라이브러리, 브라우저 API로 관리되는 상태)의 상태를 동기적으로 읽고 구독할 수 있습니다.
– useInsertionEffect (라이브러리에서 사용) : CSS-in-JS 라이브러리가 렌더링에서 스타일을 삽입할 때 발생하는 성능 문제를 해결할 수 있는 새로운 훅입니다.
[Strict Mode]
v18에서는 StrictMode가 더욱 강화되어, 개발 모드에서의 디버깅과 검사에 도움을 주는 추가적인 동작이 포함되었습니다. 그리고 렌더링에 필요한 시간, 리소스 로딩 시점 등을 더 자세히 분석할 수 있도록 돕습니다.
이 과정에서 컴포넌트가 처음 마운트될 때마다 자동으로 언마운트 및 리마운트하고 두 번째 마운트 시 이전 상태를 복원하는 새로운 개발 전용 검사를 도입합니다.
[Suspense]
v16.6에 추가된 Suspense가 v18에서 여러 개선과 기능이 추가되며 정식 기능이 되었습니다.
v17까지 Suspense는 클라이언트 사이드 렌더링(CSR)에서만 사용되었지만, v18에서는 서버 사이드 렌더링(SSR)에서도 Suspense를 사용할 수 있게 되었습니다.
이제 Suspense가 서버 스트리밍을 지원하여, 서버에서 Suspense 경계 안의 컴포넌트가 서스펜드되면 React는 해당 경계의 fallback 내용을 보내고, 이후 비동기 작업이 완료되면 클라이언트에서 트리를 다시 렌더링하여 보여줍니다. 이렇게 하여 초기 로딩을 더 빠르게 할 수 있습니다.
v18.1 ~ v18.3 버전까지는 대부분 안정성과 버그 개선들이 이루어졌습니다. 그리고 v18.3에서는 이후 있을 v19에 필요한 사용되지 않는 API 및 기타 변경 사항에 대한 경고가 추가되었습니다.
생각보다 글이 엄청 길어졌네요..
처음 생각은.. 버전 별 업데이트 내용 정리하고, Fiber 아키텍처에 대한 내용까지 적으려고 했으나..
이 부분은 다음에 새로운 글로 작성하도록 하겠습니다!