지난 3월 말에 리액트 블로그에 React 18을 런칭했다는 소식이 전해졌다.
코드를 최신 상태로 유지관리 하기 위해서는 라이브러리를 지속적으로 업데이트 해줘야 할 것이다.
런칭 이후 3개월 가량이 지난 시점이어서 어느정도 코드 안정화가 됐을 거란 판단 하에 React 17로 작성한 기존 프로젝트를 업그레이드 해보기로 했다.
컨커런트 렌더링
React 18로 버전업이 되면서 가장 핵심적인 키워드는 컨커런트(Concurrent) 렌더링이다. 리액트의 렌더링 메커니즘의 업데이트다. React 18에서 추가된 기능인 Suspense, transitions, streaming server rendering이 컨커런트 렌더링에 의존하고 있다. 컨커런트 렌더링은 리액트가 동시에 다양한 버전의 UI를 준비할 수 있음을 의미한다.
렌더링이 중단될 수 있다.
이전 버전에서는 렌더링이 동기적으로 진행되었고, 화면에 UI가 나타나기 전에는 렌더링을 중단할 수 없었다.
리액트는 메인 스레드를 방해하지 않으면서도 백그라운드 상에서 새로운 화면을 준비할 수 있게 되었다.
덕분에 무거운 렌더링이 진행중이더라도 사용자 인터랙션이 있을 때 즉각적인 UI 피드백을 줄 수 있다.
상태를 재사용 가능하다.
컨커런트 렌더링은 UI상의 특정 섹션을 제거했다가 나중에 이전 상태를 사용하여 다시 나타낼수 있다. 만약에 사용자가 탭을 바꿨다가 다시 돌아가면 이전 상태를 복원할 수 있어야 한다. 이러한 기능을 <Offscreen>이라는 컴포넌트를 사용하여 구현할 수 있다. 해당 컴포넌트를 사용하면 사용자가 어떤 화면을 열어보기 전에 해당 UI를 준비할 수 있다.
React 18의 새로운 기능들
컨커런트 렌더링은 React 18의 새로운 기능을 사용하는 부분에서만 활성화된다. 즉, 기존의 코드를 그대로 유지한 채 React 18을 도입할 수 있으며 점진적으로 컨커런트 렌더링을 적용하면 된다.
Automatic Batching
배칭은 더 나은 성능을 위해서 여러 상태 변화를 한번의 리렌더링으로 묶어서 처리하는 것이다. 이전에는 리액트 이벤트 핸들러에서만 배치되고, promisses, setTimeout, native event handlers나 다른 이벤트는 기본적으로 배치되지 않았다. 자동 배칭 덕분에 이러한 업데이트들도 자동으로 배치된다.
// 이전
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 각 스테이트 변화에 따라 두번 렌더링한다.
}, 1000);
// 자동 배치
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 자동 배치로 한 번만 렌더한다.
}, 1000);
디폴트로 자동 배치를 지원하며, 만일 자동 배칭을 비활성화하려면 flushSync를 사용하면 된다.
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// 돔 업데이트 완료
flushSync(() => {
setFlag(f => !f);
});
// 돔 업데이트 완료
}
Transitions
transition은 시급한 업데이트와 긴급하지 않은 전환 업데이트를 구분하는 개념이다. UI상에 렌더링할 업데이트 우선순위를 정해주는 것이라고 볼 수 있겠다.
- 시급한 업데이트란 타이핑, 클릭, 누름과 같이 직접적인 인터랙션을 반영한다.
- 전환 업데이트란 하나의 UI 뷰에서 다른 것으로 전환하는 것을 의미한다.
만약 사용자가 필터 버튼을 클릭한다면 이에 대한 반응은 즉각적이어야 한다. 그런데 필터 결과가 반영된 화면은 어느정도 딜레이가 있어도 사용자가 관대하게 받아들일 수 있다. 렌더링이 끝나기 전에 사용자가 다시 필터를 적용한다면 최신의 결과만 보게 될 것이다.
startTransition API를 사용해서 시급한 업데이트와 전환 업데이트를 구분할 수 있다. startTransition 안에서 정의되면 긴급하지 않은 것으로 간주되고, 다른 시급한 업데이트가 발생하면 중단될 수 있다. 만약 중단된 렌더링이 다른 시급한 상태 변화로 인해 쓸모없어졌다면 리액트는 해당 렌더 작업을 버리고 최신의 업데이트만 렌더한다.
import {startTransition} from 'react';
// 시급한 업데이트
setInputValue(input);
// startTransition 안에서 정의하면 시급하지 않은 전환 업데이트로 간주함
startTransition(() => {
setSearchQuery(input);
});
Suspense Features
Suspense는 컴포넌트 트리상의 특정 부분의 로딩 상태를 명시해준다.
리액트에서 유일하게 지원한 것은 React.lazy를 적용한 코드 스플리팅이었다. 서버사이드 렌더링에서는 지원하지 않았다.
이번 업데이트를 통해서 서버에서도 suspense를 지원하게 되었고, 컨커런트 렌더링 기능까지 확장하였다.
전환 중에 suspense하게 되면 이전의 컨텐츠가 폴백으로 나타나는 것을 방지한다. 대신에 충분한 데이터가 로드될 때까지 렌더를 지연한다.
React 18로 업그레이드 해보기
가장 최신의 React 18을 설치한다.
npm install react react-dom
yarn add react react-dom // yarn을 사용할 경우
만약 TypeScript를 사용한다면 최신 버전의 @types/react, @types/react-dom을 함께 설치해줘야 한다.
yarn.lock 파일과 package-lock.json에 18버전 이상의 @types/react와 @types/react-dom 패키지만 있어야 함을 주의하자.
ReactDOM.render는 React 18에서 더이상 지원하지 않아서 createRoot를 사용해야 한다.
// 기존
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);
// React 18
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<App tab="home" />);
@types/react 18 버전에서 변화가 꽤 크기 때문에 코드를 실행하면 에러가 발생한다. (관련 PR) 그 중에 하나는 implicit children을 제거한 것이다.
이를 바로잡으려면 Props로 전달하는 children에 React.ReactNode라는 타입 정의를 해주면 된다.
위 에러를 해결하고서도 에러가 사라지지 않았는데 애니메이션을 위해 사용하는 framer-motion 라이브러리에서 발생한 것이었다.
// 코드상 존재하는 AnimatePresence
<AnimatePresence>
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
// 에러 발생
Type '{ children: false | Element; }' has no properties in common with type 'IntrinsicAttributes & AnimatePresenceProps'. TS2559
검색을 하다가 아직 해결되지 않은 버그라는 것을 알게 되었다.
결론적으로 사용하는 라이브러리들에도 리액트 18이 적용되어 버그가 해결되는 시점에 업그레이드를 진행하기로 결정하였다.
React 18 업그레이드를 시도하며 느낀점
업그레이드를 원활히 진행하기 위해서는 앱의 디펜던시를 되도록 적게 유지하는 것이 좋겠다.
더불어 디펜던시가 적더라도 앱에 속한 코드량이 많을수록 에러 발생 지점이 많아져 허들이 될 것이다. 이를 방지하기 위해서 마이크로 프론트엔드 개념으로 코드를 쪼개서 독립적으로 관리한다면 더 수월하게 코드 수정 및 배포가 가능할 것이라고 생각한다.
TODO
타입 충돌로 인해서 React 18 적용을 미루게 되었지만, 기존에 타입스크립트로 작성되지 않은 코드는 업그레이드가 가능하지 않을까?
기존 앱에서 컨커런트 렌더링을 적용할 만한 것으로 샴푸 검색 기능이 떠올랐다. 사용자가 입력 창에 검색어를 입력하면 입력할 때마다 바뀐 검색 결과가 나타나야 한다. 검색을 클라이언트 단에서 구현했는데 사용자 입력은 즉각적이어야 하지만 입력에 따라 검색결과가 렌더링되는데는 어느정도 지연이 바람직하다고 판단했다.
- 시급한 업데이트: 사용자의 검색어 입력
- 전환 업데이트: 입력된 검색어로 필터링된 결과
Reference
https://reactjs.org/blog/2022/03/29/react-v18.html
https://blog.logrocket.com/upgrading-react-18-typescript/
'프론트엔드 개발 > 개발 로그' 카테고리의 다른 글
페이지를 벗어나기 전에 prompt 띄워주기 (react-router v6) (2) | 2022.11.21 |
---|---|
반응형 텍스트 줄바꿈 효율적으로 하기 (br 태그, wbr 태그) (0) | 2022.11.14 |
React 17에서 React 18로 업그레이드하기 (프로젝트 ver2.0 만들기) (0) | 2022.11.12 |
Headless CMS, Strapi를 적용해본 소감 (0) | 2022.09.28 |
구글 시트 API를 활용하여 프로모션 신청자 접수하기 (0) | 2022.06.18 |
댓글