풀스택을 지향하고 있다고 하나 FE 분야의 경우 기술개발의 속도가 어마어마하다. 따라서 최신 트렌드에 어떤 기술들이 발전하고 있는지에 관심을 가질 필요성을 느꼈는데 그중에서도 스터디를 진행중인 React 에서 마지막으로 React 18 버전이 나왔고 이때 동시성에 대해 중요하게 여긴 변경점이 있었다.
예를 들어 non-concurrent
환경에서는 한 번에 한가지 일을 할 수 있었으나 concurrent
환경에서는 한 번에 여러가지 일을 할 수 있어서 React 도 오래 걸리는 렌더링중에도 즉각적인 인터렉션을 제공할 수 있게 되었다는 의미이다.
아무튼 React Docs
, Vercel
, freeCodeCamp
의 문서를 기반으로 이번 React 18 에서 추가되고 향상된 기능들과 이와 관련된 React 2021 Conf
의 좋은 강연들을 참고하여 이번 포스트를 작성하였다.
이번 포스트는 처음 접하는 개념에 docs 와 conf 의 원서 내용을 번역한 내용이 많다. 자세한 내용을 더 찾아보고 싶다면 docs 를 참고하자.
React 18 특징들
CATEGORY | FEATURE |
---|---|
Concept | Concurrent React |
Features | Automatic Batching, Transitions, Suspense on the server |
APIs | createRoot, hydrateRoot, renderToPipeableStream, renderToReadableStream |
Hooks | useId, useTransition, useDeferredValue, useSyncExternalStore, useInsertionEffect |
Updates | Strict mode |
Deprecated/discouraged | ReactDOM.render, renderToString |
이미지 출처 : Vercel how react 18 improves-application-performance
브라우저에서 자바스크립트를 실행할 때 자바스크립트 엔진은 메인 스레드라 불리는 싱글 스레드 환경에서 코드를 실행하는데 메인 스레드는 작업을 하나씩 실행한다. 한 작업이 진행중이면 나머지 작업은 반드시 기다려야 하는데 보통 50ms 이상 걸리는 작업을 long task 로 간주한다. 50ms 를 기준으로 둔 까닭은 부드러운 user experience 를 위해 디바이스가 매 16ms(60fps) 마다 새 프레임을 생성해야 한다는 것에서 생긴 기준이다.
최적의 성능을 유지하기 위해 long task 의 수를 최소화해야 한다.
Total Blocking Time, TBT 는 작업을 실행하는데 50ms 이상 걸린 시간의 합으로 UX 에 큰 영향을 미친다.
리엑트의 시각적 업데이트는 render phase
와 commit phase
로 나뉜다.
render phase
: 리액트 요소들이 기존 DOM 과 비교되는 순수한 계산 단계, 새 리엑트 요소 트리가 생성되고 실제 DOM 에 대한 가상돔도 생성된다.
commit phase
: 렌더링 단계에서 계산된 업데이트를 실제 DOM 에 적용한다. 새로운 리액트 컴포넌트 트리를 미러링하기 위해 DOM 노드를 생성, 업데이트, 삭제하는 작업이 포함된다.
이미지 출처 : Vercel how react 18 improves-application-performance
기존의 렌더링 방식에서 리액트는 모든 요소에 동일한 우선순위를 부여했다. 컴포넌트의 복잡도에 따라 렌더링 시간이 길어질 수 있는데 이 시간동안 메인 스레드는 차단되어 상호작용을 시도하는 사용자들은 리액트가 렌더링 종료후 DOM 에 반영하기까지 반응을 하지 않는 UI 를 마주하게 된다.
React 18 은 화면 밖에서 동작하는 새로운 동시성 Concurrent 렌더러를 도입했는데 렌더링 별로 급한 것과 급하지 않은 것을 구분하여 우선순위를 다르게 부과하고 5ms 마다 더 중요한 작업이 있는지를 확인후 더 급한 상태 업데이트를 먼저 렌더링하는 방식이다.
하나의 중단없는 작업을 실행하는 것보다 낮은 우선순위의 컴포넌트를 리랜더링하는 동안 5ms 간격으로 메인 스레드에 제어권을 넘긴다. 동시성 렌더러는 결과를 즉시 커밋하지 않고 여러 버전의 컴포넌트 트리를 동시에 렌더링할 수 있다.
이미지 출처 : Vercel how react 18 improves-application-performance
트랜지션은 UI 업데이트가 긴급히 처리되어야 하는지 여부를 마킹할 수 있다.
urgent
: 유저에게 시각적 피드백을 주는것은 중요하므로 urgent 이다.
non-urgent
: 검색은 급히 필요한 것은 아니니 non-urgent 이다.
이러한 non-urgent
업데이트들을 transition
이라고 부른다. non-urgent
UI 업데이트에 transition 이라고 마킹하는 것을 통해 리액트는 무엇이 우선순위가 높은지를 알 수 있다.
import { startTransition } from 'react';
// Urgent: 입력한 내용을 표시하는 것
setInputValue(input);
// 트랜지션 내에서 non-urgent 상태 업데이트를 마킹
startTransition(() => {
// Transition: 결과를 보인다.
setSearchQuery(input);
});
startTransition
: non-urgent 라고 마킹할 수 있다.
트랜지션이 debouncing 과 setTimeout 과 다른점은?
- startTransition 은 setTimeout과 달리 즉시 실행된다.
- setTimeout 이 보장된 지연을 갖는다면 startTransition 의 지연은 디바이스의 속도, 다른 급한 렌더링에 의존적으로 결정된다.
- startTransition 업데이트는 setTimeout 과 달리 방해받을 수 있고 페이지가 동결되지 않는다.
- 리액트는 startTransition 으로 마킹된 pending state 를 추적할 수 있다.
import { useTransition } from "react";
function Button() {
const [isPending, startTransition] = useTransition();
return (
<button
onClick={() => {
urgentUpdate();
startTransition(() => {
nonUrgentUpdate()
})
}}
>...</button>
)
}
상태 업데이트를 startTransition 으로 감싸서 특정 렌더링을 연기하거나 중단해도 된다고 리액트에게 알릴 수 있다.
이미지 출처 : Vercel how react 18 improves-application-performance
사용자의 입력이나 이벤트 발생시 일정 시간 동안 중복 호출을 방지하는 기술이며 주로 검색창이나 스크롤 이벤트 등에 활용되고 사용자의 빠른 연타로 인한 불필요한 동작이 발생하는 것을 방지하고 서버 또는 함수 호출의 불필요한 부하를 줄일 수 있다.
사례
1. state, useEffect 훅을 이용한 방법
import React, { useState, useEffect } from 'react';
function DebounceExample() {
const [inputValue, setInputValue] = useState('');
useEffect(() => {
const delay = 300; // 디바운스 대기 시간 (밀리초)
const timeoutId = setTimeout(() => {
// 입력값을 처리하는 함수 호출
console.log('입력값 처리:', inputValue);
}, delay);
return () => clearTimeout(timeoutId); // 이전 타임아웃 제거
}, [inputValue]);
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
return (
<input
type="text"
value={inputValue}
onChange={handleInputChange}
placeholder="검색"
/>
);
}
export default DebounceExample;
입력값이 바뀔 때마다 디바운스 시간 이후 입력값을 처리하는 함수가 호출되는 예시다.
전통적으로 리액트는 앱을 렌더링하는 방법을 제공해왔다. 모든 것을 완전히 클라이언트가 렌더링하거나 (클라이언트 사이드 렌더링) 컴포넌트 트리를 서버에서 HTML 로 렌더링하고 정적 HTML 과 컴포넌트 Hydrate 에 사용될 자바스크립트 번들을 클라이언트 측에 전송할 수도 있다. (서버 사이드 렌더링)
이미지 출처 : Vercel how react 18 improves-application-performance
그러나 두 방식 모두 제공된 자바스크립트 번들을 사용하는 컴포넌트 트리를 클라이언트 측에서 다시 빌드해야 한다.
서버 사이드 렌더링의 한계
오늘날 클라이언트 측 JS 의 Server side rendering 은 최적이 아닐 수 있다. 자바스크립트로 구성된 컴포넌트는 서버에서 HTML 문자열로 렌더링된다.
하지만 여전히 상호작용을 위해 JavaScript가 가져와져야 하며, 이는 일반적으로 수분화(hydration) 단계를 통해 달성된다. 서버 측 렌더링은 일반적으로 초기 페이지 로드에 사용되며, 수분화 이후에는 다시 사용되지 않는 경우가 많다.
React Server Components를 사용하면 컴포넌트를 정기적으로 다시 가져올 수 있다. 새 데이터가 있는 경우 컴포넌트가 다시 렌더링되는 응용 프로그램을 서버에서 실행할 수 있으며, 클라이언트로 전송되는 코드 양을 제한할 수 있다.
Server Component
서버 컴포넌트는 Server side rendering 을 보완하여 intermediate abstraction format 으로 렌더링하는 기능을 제공하고 JS 번들을 추가로 늘리지 않아도 된다.
서버 컴포넌트는 SSR 의 대체품이 아니다. 두 기능을 함께 사용하여 중간 형식으로 빠르게 렌더링한 후, Server side rendering 인프라가 이를 HTML 로 렌더링하여 초기 렌더링 속도를 유지할 수 있다.
이미지 출처 : Vercel how react 18 improves-application-performance
이미지 출처 : dev.to - What the heck is SSG: Static site generation explained with Next.js
CSR, SSR 아주 자주 들어봤을 용어지만 물흐르듯 설명할 수 있을까? 지금 자세히 알아보자.
렌더링
: HTML, CSS, JS 등으로 작성한 문서가 브라우저에 출력되는 과정을 말한다.
CSR 은 서버에서 전체 페이지를 최초에 렌더링하고 사용자의 요청이 발생할 때마다 클라이언트 (브라우저)에서 렌더링하는 것을 말한다.
React 와 함께하는 대부분의 애플리케이션 로직들은 클라이언트에서 실행되며 데이터를 fetch 혹은 save 하기 위해 API 호출을 통해 서버와 상호작용한다. 그래서 대부분의 UI 는 클라이언트에서 생성된다.
전체 웹 애플리케이션이 첫 요청때 로드된다. 유저들이 링크를 클릭하여 navigate 하기 때문에 페이지를 렌더링하기 위해 서버로의 새로운 요청이 생성되지 않는다. view 혹은 데이터를 바꾸기 위해 코드는 클라이언트에서 실행된다.
CSR 은 우리가 페이지 새로고침 없이 navigation 을 지원하며 훌륭한 UX 를 제공하는 SPA를 가질 수 있게 한다. CSR 은 또한 개발자들에게 client 와 server 코드의 분명한 구분을 제공한다.
단점
1. SEO : 페이지의 복잡도가 높아졌을때 컨텐츠가 충분히 렌더링되어 있지 않은 상태로웹 크롤러가 인덱스하게 되어 크롤러가 JS를 이해하는데 제한이 발생하게 된다.
2. 성능 : 브라우저가 컨텐츠를 처음 렌더링하기 위해서 JS 가 로드되기까지 대기해야 하며 이는 유저들이 초기 페이지 로드시 약간의 랙을 경험하게 된다.
CSR 성능을 향상시키는 방법
Preloading
: 페이지에서 필요로 할 중요한 자원들을 페이지 생성주기보다 먼저 로드하는 기법이다. 중요한 자원에는 아래의 태그를 head 섹션 내에 포함시켜서 미리 로드될 수 있는 JS 등을 포함한다. 이는 브라우저에서 페이지 렌더링이 시작되기 이전에 JS 파일의 로드가 시작된다는 것을 말한다.Lazy Loading
: 크게 중요치 않은 리소스를 확인할 수 있고 이들은 필요할때만 로드하는 방법이다. 이 기법을 사용하면 초기에 로드되는 리소스의 양이 줄어드니 초기 로드 속도가 향상된다.Code Splitting
: JS 코드의 큰 번들을 피하기 위해 번들을 쪼갤 수 있다. 이 과정은Webpack
과 같은 번들러를 통해 수행할 수 있고 이를 통해 런타임에 동적으로 로드될 수 있는 다수의 번들을 생성할 수 있다.
<link rel="preload" as="script" href="critical.js" />
SSR 은 서버에서 사용자에게 보여줄 페이지를 모두 구성한뒤 페이지를 렌더링하는 방식을 말한다.
SSR 은 사용자의 요청에 대항 응답으로 렌더링될 full HTML 페이지 콘텐츠를 생성한다. DB 에 대한 연결과 fetch 는 서버에서 다뤄지며 콘텐츠를 포멧하기 위해 필요한 HTML 은 또한 서버에서 생성된다. 따라서 렌더링 코드가 클라이언트에 필요하지 않으며 이에 해당하는 자바스크립트를 클라이언트에 보낼 필요가 없다.
SSR 과 함께 모든 요청은 독립적으로 취급되며 서버에 의해 새로운 요청으로서 진행된다.
페이지에 다수의 UI 엘리먼트와 애플리케이션 로직들이 있는 경우 SSR 은 CSR 과 비교시 더 적은 JS 를 갖는다. 이에 따라 로드에 필요한 시간과 스크립트는 더 적게 필요하다.
장점
- 페이지를 렌더링하는데 필요한 JS 가 적으니 애플리케이션에 의해 필요로 하는 서드파티 JS를 위한 여분 공간을 추가할 수 있다.
- 페이지를 서버에서 모두 구성한뒤 브라우저로 보내기에 검색 엔진 크롤러가 SSR 애플리케이션의 콘텐츠를 크롤링하기 쉬워 SEO 에 강점을 보인다.
단점
- 모든 프로세스가 서버에서 이루어지기에 느린 네트워크, 서버코드가 최적화되지 않는 경우, 다수의 유저가 동시에 서버로 로드를 하려고 할때 서버로부터의 응답이 지연될 수 있다.
- 새로고침할때 약간의 깜빡임이 발생한다.
Pros and Cons | CSR | SSR |
---|---|---|
장점 | 페이지간 이동시 속도가 빠르다. 필요한 내용과 수정된 데이터만 교체하므로 속도가 빠르다 새로고침이 없어 화면 깜빡임이 발생하지 않는다. TTV 와 TTI 의 공백기간이 짧다 | 사용자에게 보여지는 초기 페이지 렌더링 속도가 빠르다 SEO 에 강점을 갖는다. |
단점 | 초기 페이지 렌더링 속도가 느리다. SEO 에 불리하다. | 화면의 깜빡임이 발생한다. |
TTV(Time To View)
: 사용자가 화면을 보는 시점
TTI(Time To Interact)
: 사용자가 실제로 서비스를 이용하는 시점
hydration : 수화, 몸에 수분을 보충하는 행위
render 메소드
ReactDOM.render(element, container[, callback])
이 render 함수는 컨테이너의 자식으로 리액트 컴포넌트를 넣어주는데 기존에 이미 렌더링 된 리액트 컴포넌트가 있으면 새로 렌더링 하는게 아니라 업데이트만 해준다. 그리고 렌더링이 완료되면 세번째 인자로 전달된 콜백이 실행되게 할 수 있다.
즉, ReactDOM 의 render함수는 컴포넌트를 렌더링한 후에 콜백을 실행한다.
hydrate 메소드
ReactDOM.hydrate(element, container[, callback])
hydrate는 렌더링은 하지 않고 이벤트 핸들러만 붙여준다. 서버사이드 렌더링을 해서 이미 마크업이 채워져 있는 경우에는 굳이 render 메소드를 사용할 필요가 없다. SSR을 하는 경우에는 hydrate로 콜백만 붙여야 한다.
CSR를 하는 경우에는 타겟 컨테이너에 리액트 컴포넌트가 렌더링 된 적이 없을것이기 때문에 render메소드를 사용해야 한다. 하지만 SSR 프레임워크와 함께 리액트를 사용할때는 hydrate 사용을 고려해야 한다.
서버가 완성된 HTML을 내려준다. 이때 Dehydrate는 수분을 없앤다는 뜻이다. 다시 말해서 동적인것을 정적으로 만드는 행위를 Dehydrate라고 한다. 그리고 나서 JS가 실행되면서 리액트가 정적인 HTML과 store를 동적인 리액트 컴포넌트 트리와 store로 변환하는 과정이 일어나는데, 이걸 (Re)hydrate라고 한다.
마치 수분기 없는 정적인 상태에서 수분 넘치는 동적인 상태로 변화한 것이다. 문제는 이렇게 rehydrate가 일어나면서 쓸데없이 화면이 한 번 더 그려지는 현상이 발생한다는 것이다. 왜냐면 리액트는 서버에서 완성된 HTML이 내려와서 이미 화면에 제대로 렌더링이 됐는지 안됐는지 모르고 자신이 할 일을 그냥 했을 뿐이다. 그래서 SSR을 하는 경우에는 ReactDom의 render 메소드가 아니라 hydrate 메소드를 사용해야 한다.
리액트에서 hydration이라고 하는 용어를 사용하는 이유는 "서버사이드 렌더링으로 만들어진 수분이 없는 정적인 HTML과 State로부터 수분을 보충하는 과정(동적인 상태로 변화)인 hydrate가 일어나기 때문" 이지 않을까?
이미지 출처 : Vercel how react 18 improves-application-performance
원격 소스에서 데이터가 로드되는 등의 특정 조건을 만족시킬 때까지 컴포넌트 렌더링을 지연시킬 수 있다. 명시적으로 로딩 상태를 정의하여 조건부 렌더링 로직의 필요성이 줄어든다.
suspense
를 리액터 서버 컴포넌트와 함께 사용시 DB 나 파일 시스템과 같이 별도의 API 엔드포인트가 없어도 서버 측 데이터 소스에 직접 접근할 수 있다.
리액트는 사용자 상호작용에 따라 컴포넌트의 우선순위를 조절하여 현재 진행중인 렌더링을 일시 정지하고 사용자가 상호작용한 컴포넌트의 우선순위를 높여서 진행할 수 있다. 완료후 DOM 에 커밋하면 이전 렌더링을 재개한다. 이런 과정을 통해 사용자 상호작용을 우선 처리하고 UI 가 입력에 따라 최신상태를 유지할 수 있게 한다.
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
React 18 에서의 suspense 는 transition API 와 결합할때 최고의 동작을 한다. 만약 트랜지션동안 지연되었다면 리액트는 이미 볼 수 있는 콘텐츠를 fallback 에 의해 교체되는 것을 방지한다. 그 대신 리액트는 나쁜 로딩 상태를 예방하기 위해 데이터가 충분히 렌더링될때까지 지연시킨다.
데이터 렌더링 외에도 React 18 에서 데이터를 패치하고 결과를 효율적으로 기억하는 새로운 API 를 도입했다
React 18 은 래핑된 함수 호출의 결과를 기억할 수 있는 캐시함수가 있어 동일한 렌더링 단계서 같은 함수, 같은 인자를 이용해 호출한다면 다시 호출하는게 아니라 기억한 값을 사용한다.
import { cache } from 'react'
export const getUser = cache(async (id) => {
const user = await db.user.findUnique({ id })
return user;
})
getUser(1)
getUser(1) // 동일한 렌더링 단계서 호출될때 memoized 한 결과를 반환한다.
fetch 호출시엔 cache 없이 비슷한 캐싱 메커니즘을 포함하는데 이는 같은 렌더링 단계서 네트워크 요청의 수를 줄이고 애플리케이션의 성능을 향상하고 API 비용을 낮춘다.
export const fetchPost = (id) => {
const res = await fetch(`https://.../posts/${id}`);
const data = await res.json();
return { post: data.post }
}
fetchPost(1)
fetchPost(1) // 동일한 렌더링 단계서 호출될때 memoized 한 결과를 반환한다.
Context API 에 접근할 수 없는 리액트 서버 컴포넌트와 함께 사용시 유용한데 cache와 fetch의 자동 캐싱을 통해 애플리케이션에서 전체적으로 재사용할 수 있다.
이미지 출처 : Vercel how react 18 improves-application-performance
async function fetchBlogPost(id) {
const res = await fetch(`/api/posts/${id}`);
return res.json();
}
async function BlogPostLayout() {
const post = await fetchBlogPost('123');
return '...'
}
async function BlogPostContent() {
const post = await fetchBlogPost('123'); // memoized 된 값을 반환한다.
return '...'
}
export default function Page() {
return (
<BlogPostLayout>
<BlogPostContent />
</BlogPostLayout>
)
}
memoize
: 동일한 계산을 반복해야 할때 이전에 계산했던 값들을 메모리에 저장해서 반복 수행을 제거하여 프로그램의 실행 속도를 향상시킬 수 있는 방법이다.
결과를 캐싱하고 다음 작업에서 캐싱한 것을 재사용하는 것을 통해 작업의 속도를 높일 수 있다.
리액트 훅중 메모이제이션 기능을 지원하는 기능은 다음과 같다.
useMemo
: 메모이제이션된 값을 반환해준다useCallback
: 메모이제이션된 콜백을 반환해준다.
Automatic batching
- Batch, 일괄처리는 React 가 성능 향상을 위해 다수의 상태 갱신을 더 나은 성능을 위해 단일 리렌링으로 처리하는 것을 말한다.
- 자동배치 없이는 React 의 이벤트 핸들러 안에서만 일괄처리해왔다.
- 그러나 프로미스, setTimeout, native event handler, 그리고 다른 이벤트들은 default 로 배치되고 있지 않았다.
- 자동배치를 통해 이러한 업데이트들이 자동적으로 배치된다.
// 기존: 오직 React 이벤트만 batch 된다.
setTimeout(() => {
setCount(c => c + 1); // re-render
setFlag(f => !f); // re-render
// React 는 두번 렌더링 될 것이고 batch 없이 각 상태가 변할때마다 렌더링된다.
}, 1000);
// v18: setTimeout, promises, native event handler 혹은 다른 이벤트들도 batch 된다.
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 는 오직 마지막에 한번 re-render 된다.(이게 batch다)
}, 1000);
기존엔 상태가 바뀔때마다 재렌더링되었다. v18 에선 배치를 수행하므로 마지막에 한번만 렌더링된다.
setTimeout
만으로 예시가 부족할까봐 하나 더 가져와봤다.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // 아직 재랜더링 되지 않는다.
setFlag(f => !f); // 아직 재랜더링 되지 않는다.
// React 는 마지막에 한번 재랜더링된다. 이게 배치다.
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
클릭이벤트 한번에 count 와 flag 상태가 모두 바뀌니 기존엔 두 번 렌더링되었다. 이제 React 는 이 부분에 대해 단일 렌더링만 수행한다.
장점
automatic batching
은 불필요한 재렌더링을 방지하기에 성능에 좋다.flushSync
를 통해 batch
를 거부할 수 있다.그러나 기존 React 는 batch update 시 일관성이 없었다. 위의 예시에서 fetch data 를 할 경우 업데이트를 batch 하지 않고 2개의 독립적인 업데이트를 수행해왔다. 이는 React 가 click 과 같은 브라우저 이벤트 동안 업데이트를 batch 해왔으나 위의 사례의 경우엔 event 가 이미 처리된 후에 fetch callback 에서 상태를 변경하고 있기 때문이다. (during 이 아니라 after 라서 그렇다)
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 17 및 그 이전 버전은 이들을 batch 하지 않는데 이는
// 이 코드가 이벤트 도중이 아닌 이벤트가 종료된 이후에 콜백을 실행하기 때문이다.
setCount(c => c + 1); // 재랜더링
setFlag(f => !f); // 재랜더링
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
React 18 이 나오기 전까지 오로지 React event handler 내에서 업데이트를 갱신해왔다.
createRoot
로 React 18 을 사용하면 모든 update 들은 그들이 어디에 있든지간에 상관없이 자동적으로 batch 된다.
// 마지막에 한번만 재랜더링을 하게된 사례들
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);
fetch(/*...*/).then(() => {
setCount(c => c + 1);
setFlag(f => !f);
})
elm.addEventListener('click', () => {
setCount(c => c + 1);
setFlag(f => !f);
});
React 18 에서 모든 update 는 자동적으로 batch 된다는데 그걸 원하지 않다면?
만약 어떤 코드들이 상태 변화 이후 즉각적으로 DOM 으로부터 무언가를 읽어야 한다면 ReactDOM.flushSync() 메서드를 사용해서 배치가 되지 않도록 해줄 수 있다.
import { flushSync } from 'react-dom'; // Note: react-dom, not react
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React 는 지금 DOM 을 업데이트
flushSync(() => {
setFlag(f => !f);
});
// React 는 지금 DOM 을 업데이트
}
흔한 케이스는 아닐것 같다.
Suspense on the server
애플리케이션 개발자로서 서버 사이드 렌더링 측면에서 suspense 를 지원하는 것에 대해 관심이 있을 수 있다.
server rendering
서버로부터 클라이언트에 HTML 을 보낼때 우리의 앱이 상호작용되기 전에 사용자는 Javascript 번들이 로딩되는 동안 어떤 UI 를 볼 수 있다.
만약 하나의 특정 컴포넌트를 제외하고 앱의 대부분이 빠르다면 이 컴포넌트는 데이터를 느리게 로드할 것이고 이 컴포넌트는 아마도 상호작용하기 전에 많은 Javascript 를 실행해야 할 것이다.
React 18 이전엔 이는 bottleneck 이라 불리는 병목현상을 야기할 수 있었고 이 컴포넌트를 통해 지연시간이 증가했다. 한 컴포넌트가 전체 앱을 느리게 만들 수 있다는 것이다. 이는 서버사이드 렌더링이 All or nothing 방식이기 때문이다.
그러나 React 18 부터는 서버에서 suspense
를 지원한다.
이게 뭘 의미하냐면, suspense
의 도움을 통해 이제 애플리케이션에서 느린 파트를 감쌀 수 있고 react 에게 이 로딩을 지연시켜달라고 말할 수 있다는 것이다.
suspense 는 마찬가지로 특정 상태를 로딩하는데에도 suspense 를 사용하여 위의 경우 댓글이 로딩되는 동안 spinner 를 보여줄 수도 있다.
따라서 React 18 부터는 특정 컴포넌트가 더이상 전체 앱을 느려지게 할 수 없다. suspense
를 통해 react 에게 로딩 spinner 와 같은 다른 컴포넌트들을 먼저 보내달라고 요청할 수 있다.
suspense 는 user experience 를 높여줄 수 있다.
- 하나의 느린 파트가 더이상 전체 페이지를 느리게하지 않는다.
- 초기 HTML 을 먼저 보이고 나머지를 stream 한다.
- React 18 에선 서버 렌더링은
code splitting
이라 불리는 최적화 방식과 전체적으로 통합되었다.
New APIs for app and library developers
React 18 은 concurrent features 라고 불리는 새로운 API 들을 추가했다. 이들은 브라우저를 responsive 하게 유지하기 위해 몇몇 상태가 non-urgent 로 업데이트된다고 마킹하는 것을 통해 애플리케이션 성능을 나타낼 수 있다.
New APIs concurrent features
- startTransition()
- useTransition() : 일부 상태 업데이트를 긴급하지 않은 것으로 표시할 수 있다.
- useDeferredValue() : 트리의 긴급하지 않은 부분의 재랜더링을 연기할 수 있다.
New APIs mostly for libraries
- useId() : 컴포넌트를 위한 unique IDs 를 생성
- useSyncExternalStore() : 저장소에 대한 업데이트를 강제로 동기화하여 외부 저장소가 동시 읽기를 지원할 수 있도록 하는 새로운 hook 이다.
// react, react-dom 설치
npm install react react-dom
변경점들
// 기존
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);
// v18
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); // createRoot(container!) 타입스크립트 사용시
root.render(<App tab="home" />);
// 기존
unmountComponentAtNode(container);
// v18
root.unmount();
// 기존
const container = document.getElementById('app');
render(<App tab="home" />, container, () => {
console.log('rendered');
});
// v18
function AppWithCallbackAfterRender() {
useEffect(() => {
console.log('rendered');
});
return <App tab="home" />
}
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<AppWithCallbackAfterRender />);
suspense 사용시 콜백이 의도와 다르게 동작하기도 했으니 render 로부터 콜백을 제거했다.
// 기존
import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);
// v18
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
// Unlike with createRoot, you don't need a separate root.render() call here.
만약 앱을 hydration
과 함께 서버 사이드 렌더링을 사용중이라면 hydrate
를 hydrateRoot
로 업데이트하자.
docs
react-dev - react-v18
react-dev - react-v18-upgrade-guide
Vercel - How React 18 Imporves Application Performance
freeCodeCamp - React 18 New Features - Concurrent Rendering, Automatic Batching, and More
patterns.dev - client-side rendering
patterns.dev - server-side rendering
patterns.dev - react server component
conf
React Conf 2021 - React 18 for app developers by Shruti Kapoor
blog
sehyunny - Vercel 의 <어떻게 React 18 이 앱 성능을 개선시켰나> 라는 포스트를 번역
taeeeeun - React 18 달라진점
adjh54 - CSR & SSR
처음 개념들을 접하는 순간은 정보의 범람속에 빠진 느낌이었다. 나는 개념을 글로 보는 것보다 실제로 써봐야 이해할 수 있는 타입이라 바로 이해가 된다고 할수는 없다. 그래도 이곳저곳 찾아봐야 알 수 있는 내용들을 나의 블로그에 통합하여 정리해두는 습관들을 통해 실제로 react 를 적극적으로 활용할때 이 포스트를 토대로 원리를 이해할 수 있을 것 같다.
포스트를 작성하다 patterns.dev, Vercel dev blog 라는 아주아주 괜찮은 사이트를 알게 되어 ALL 시리즈의 DOCS 에 추가했다. 이 사이트들은 두고두고 최신 트렌드에 대해 알아보려 할때 자주 볼 예정이다.
알고보면 최근 트렌드에 대해 처음으로 찾아보고 정리한 포스트다. 하나씩 추가해보자. 👍