Concurrent (동시성)createRootAuto BatchingTransitionsuseTransitionuseDeferredValueSSRSuspenseStreaming HTMLReact.lazySelective HydrationNew HooksuseIduseSyncExternalStoreuseInsertionEffect
React v18 에서 변경된 사항들을 예제를 실행해보면서 정리해보려고 한다.
글로만 적는 것보다 실제로 동작해보는 게 공부가 될 것 같아 codesandbox 를 통해 예제 코드를 작성하면서 포스팅하였다.
Concurrent (동시성)
동시성을 전화 통화로 비유한 글이 있다. [링크]
자바스크립트는 싱글 스레드이기 때문에 여러개의 작업을 동시에 실행할 수 없는데 React v18에서는 여러 개의 작업이 있는 경우 작업들을 번갈아 가면서 진행시켜 동시에 실행하는 것처럼 동작할 수 있도록 해준다.
createRoot
React v17 에서 사용되던
ReactDOM.render
가 deprecated 되고 ReactDOM.createRoot
로 대체되었다. createRoot 를 적용하여 동시성 및 Auto Batching 를 적용할 수 있다.React v17
React v18
Auto Batching
일괄 처리(Batching) 를 통해 불필요한 리렌더링을 줄여준다. 예를 들어 하나의 이벤트 핸들러에 2개의 setState 가 실행되는 경우, 기존에는 상태 값이 2개가 변경되기 때문에 리렌더링이 2번 발생하여 불필요한 렌더링이 발생하게된다. 이런 경우에 변경되는 항목들을 하나로 묶어 일괄 처리하게 되면 리렌더링이 한 번만 실행된다.
기존 v17 에서도 해당 기능을 지원했지만 setTimeout 이나 fetch 내부에서는 지원을 하지 않았다.
예제 컴포넌트
v17
button1 을 클릭하면 해당 결과가 나온다.
Batching 이 정상적으로 적용되어 2가지 상태를 변경해도 한 번만 리렌더링이 되는 것을 확인할 수 있다.
하지만 setTimeout 을 통해 setState 를 실행하는 button2 를 클릭하면 해당 결과가 나온다.
Batching 이 적용되지 않고 리렌더링이 2번 실행되는 것을 확인할 수 있다.
v18
button1, button2 모두 batching 이 적용된 결과값이 나오는 것을 확인할 수 있다.
Auto Batching 을 사용하고 싶지 않은 경우
ReactDOM.flushSync()
를 사용하여 해제할 수 있다.Transitions
Transitions 는 긴급 업데이트(Urgent updates) 와 전환 업데이트(Transition updates) 로 구분할 수 있는 개념이다. 만약 사용자가 웹 페이지에서 어떤 동작을 하는 경우 사용자의 이벤트에 의해 즉각적으로 반응해주지 않으면 버그로 처리되는 항목을 긴급한 것으로 간주한다.
useTransition
isPending
값을 통해 긴급한 업데이트가 실행 중이면 true 가 반환되어 해당 값을 통해 조건부 렌더링을 처리해 줄 수 있고, startTransition()
를 통해 인자값으로 함수를 전달하여 긴급 업데이트가 끝난 이후에 실행시켜준다.예시 코드를 보면서 설명해보려고 한다.
해당 코드는 긴급 업데이트를 해야 하는 검색 input 의 value 값이 있고, 그 값을 렌더링해주는 전환 업데이트 값(searchQuery)이 있는 상황이다. (이때 searchQuery 값을
map()
을 통해 5000번 렌더링 해주고 있다.)useTransition
을 사용하기 전에는 이런 식으로 결과가 보여질 것이다. (연속해서 타이핑을 하는중이다.)이제
useTransition
을 적용한 결과값을 보면이런 식으로
- 긴급 업데이트인 input 값을 우선적으로 렌더링
- searchQuery 값 렌더링 대기 (로딩)
- searchQuery 값 렌더링
순서로 진행되는 것을 확인할 수 있다.
확실히
startTransition
을 적용한 결과가 우선적으로 보여줘야 하는 항목과 아닌 부분을 구분해주니 UX 가 항상 된 것 같다. 그리고 debouncing 과 throttling 이랑 다르게 긴급 업데이트가 진행이 되면 바로 실행이 되는 구조를 가지고 있기 때문에 쓸데없이 낭비되는 시간이 없어 성능적으로도 좋고 명확하게 렌더링을 해줄 수 있다.useDeferredValue
useDeferredValue
도 startTransition
과 비슷하게 우선 순위를 구분해줄 수 있지만 startTransition
은 인자로 전달된 함수의 실행을, useDeferredValue
는 특정 값의 업데이트를 지연시켜준다.예제 코드를 보면서 설명해보려고 한다.
startTransition
의 예제와 마찬가지로 input 의 value 값과 검색값인 searchQuery 값이 있는 상태에서 useDeferredValue
를 통해 deferredQuery 라는 변수를 생성하였다. 그리고 이 값을
useMemo
을 사용해서 변수를 한 번 더 생성하였다.이제
useEffect
의 deps 에 input의 value 와 useDeferredValue
와 useMemo
를 통해 생성한 sQuery 를 콘솔을 통해 확인을 할 수 있는 코드를 작성하였다.실행해보면 이런 결과가 보인다.
콘솔 값을 더 자세히 살펴보면
2번째 콘솔을 보면 value 는 긴급 업데이트 값이기 때문에 1234 를 입력함과 동시에 상태 값에 적용되었지만, 우선순위가 낮은 sQuery 값은 3번째 콘솔에서 적용이 되는 것을 확인할 수 있다. 이처럼 특정 값의 우선순위를 뒤로 지연시켜 긴급 업데이트의 값이 적용된 이후에 업데이트를 진행시켜 줄 수 있다.
SSR
이전 React 버전의 SSR 은 이런 순서로 페이지 렌더링이 진행되었다.
- 서버에서 렌더링에 필요한 데이터를 받음 (Data fetching)
- 서버에서 HTML 을 렌더링 후 클라이언트에 전달
- 자바스크립트 로드
- Hydration (서버에서 생성한 HTML 과 자바스크립트를 결합하는 것)
하지만 이 순서로 진행되면 문제점들이 몇 가지가 있다.
- 특정 컴포넌트의 데이터를 가져오는것이 오래 걸리는 경우
- 특정 컴포넌트의 로드가 오래걸리는 경우
- 특정 컴포넌트의 코드가 복잡하여 렌더링 시간이 오래 걸리는 경우
렌더링 과정들은 순차적으로 실행되기 때문에 진행되는 과정이 완료되어야 다음 과정으로 진행할 수 있어 병목 현상이 발생해 성능이 매우 낮아지는 문제가 생긴다. 이 문제들을 React v18 에서 해결할 수 있다.
Suspense
데이터를 fetching 해서 상태 값에 적용시킨 뒤 해당 데이터를 렌더링하는 경우의 코드를 간단하게 작성해보았다.
하지만 이런 식으로 작성하면 문제점이 몇 가지가 있다.
- 하나의 컴포넌트에 Loading 컴포넌트와 기존(Post) 의 컴포넌트가 같이 있어 가독성이 좋지 않다.
- 리액트에서 추구하는 선언형 프로그래밍이 아닌 명령형 프로그램으로 진행된다.
적용시킬 컴포넌트에 Suspense 를 감싼 뒤
<Suspense/>
의 fallback
에 로딩 시 표시할 JSX 항목을 적용시켜주는 형식이다.Streaming HTML
Suspense
를 사용하면 다른 컴포넌트와 별개로 렌더링이 진행된다.예를 들어 Comments 컴포넌트를
Suspense
로 감싼 코드가 있는 경우에 렌더링이 진행되면 이런 식으로 코드가 보일 것이다.렌더링이 진행되고 있기 때문에 Comments 컴포넌트는
fallback
에 적용한 Spinner 컴포넌트가 보여지고 있다.그 이후 Comments 컴포넌트의 렌더링이 완료되면 기존에 있었던 Spinner 컴포넌트 대신에 기존의 Comments 컴포넌트의 코드가 보여진다.
이를 통해 다른 컴포넌트와 별개로 렌더링이 진행되기 때문에 서버에서 렌더링에 필요한 데이터를 가져오는 경우 특정 컴포넌트의 데이터를 가져올 때 오래 걸리는 문제 (문제점1) 을 해결하였다.
이제 문제점 2,3 번이 남았다.
- 특정 컴포넌트의 로드가 오래걸리는 경우
- 특정 컴포넌트의 코드가 복잡하여 렌더링 시간이 오래 걸리는 경우
React.lazy
React.lazy
는 v18 이전에도 Suspense
와 함께 사용하여 코드 스플리팅을 적용시키는데 사용되었지만 SSR 환경에서는 별도의 라이브러리를 통해서만 사용이 가능했었다.v18 부터는
pipeToNodeWritable
를 통해 SSR 환경에서도 사용이 가능해졌다.React.lazy
와 Suspense
를 사용하여 Comments 컴포넌트와 다른 컴포넌트들의 렌더링을 분리하여 다른 컴포넌트들이 Comments 를 기다리지 않고 hydration 까지 진행을 할 수 있게 되었다.이로 인해 문제점2, 3번이 해결되었다.
Selective Hydration
Suspense
를 2번 사용한 경우를 살펴보자.DOM 트리의 순서대로 Sidebar 가 먼저 hydration 이 진행되는데 만약 사용자가 Comments 컴포넌트를 클릭하면(인터렉션) Sidebar 컴포넌트의 hydration 을 일시 정시하고 Comments 컴포넌트를 hydration 하게 된다. 이처럼 사용자의 인터렉션에 따라 hydration 의 우선순위를 결정할 수 있다.
New Hooks
useId
useId hook 을 사용해서 고유 id 값을 생성하여 클라이언트와 서버간의 고유 id 가 hydrate 과정에서 매칭되지 않는 이슈를 해결할 수 있다.
useSyncExternalStore
React v18 의 동시성으로 인해 UI 의 렌더링이 일시정지 되는 것과 관련하여 해당 UI 에 연관된 외부 저장소(redux, recoil 등)의 상태 값을 동기화 시켜준다. (Tearing 방지)
subscribe
: 스토어가 변경될 때마다 실행되는 함수
getSnapshot
: 스토어의 상태값을 반환하는 함수
getServerSnapshot
: 서버 사이드 렌더링 시 snapshot 의 값을 반환하는 함수
useInsertionEffect
해당 hook 은 라이브러리에서 사용된다고 한다.
css-in-js 라이브러리가 렌더링을 할 때 스타일을 삽입하는데 이때의 성능을 향상시켜준다.
간단하게나마 정리해봤는데 더 파고들어야 이해되는 개념들이 많은 것 같아 더 공부하면서 포스팅을 새로 하거나 해당 글을 수정하는 방향으로 계속 진행해야 될 것 같다.
ETC
- React v18 환경에서 위와 같이
ReactDOM.render
를 사용하니까 콘솔에 에러 메시지가 나왔다.
Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot
v18 에서는
ReactDOM.render
를 지원하지 않기 때문에 생성되는 것 같다.출처