본문 바로가기
React-study/dil

[모던 리액트 Deep Dive] 10장 리액트 17과 18의 변경 사항 살펴보기

by 어느새벽 2024. 12. 9.

 

10.1 리액트 17 버전 살펴보기

리액트 17버전은 16버전에서 더 추가된 기능은 없고 기존에 사용하던 코드의 수정을 필요로 하는 변경 사항을 최소화했다는 점이 가장 큰 특징이다.

 

10.1.1 리액트의 점진적인 업그레이드

16에서 더 이상 호환되지 않는 API가 있거나 새로운 리액트 17을 사용하는 데 있어 이전과 작동 방식이 달라질 수 있기 때문에 단행된 주 버전 업데이트다.

전체 애플리케이션 트리는 리액트17이지만 일부 트리와 컴포넌트에 대해서는 리액트18을 선택하는 점진적인 버전 업이 가능해진다.

 

한 애플리케이션 내에 여러 버전의 리액트가 존재하는 경우 예제 659p

리액트 17 애플리케이션은 내부에서 리액트 16을 게으르게(lazy) 불러온다.

불러오는 과정에서 리액트 16을 위한 별도의 루트 요소를 만들고,

불러온 리액트 16 모듈을 렌더링하는 구조로 구성돼 있다.

-> 서로 렌더링하는 과정에서 버전의 불일치로 인한 에러가 발생하지 않고 두 버전이 공존하게 된다.

 

10.1.2 이벤트 위임 방식의 변경

위임 방식의 변화를 이해하기 전 리액트에서 이벤트가 어떻게 추가되는지 이해해야 한다.

예제 663p

리액트 버튼 : DOM에 이벤트를 추가하는 방식으로 onClick 이벤트 추가

그냥 버튼 : 직접 DOM을 참조해서 가져온 다음, DOM의 onClick에 직접 함수를 추가하는 고전적이 방식

 

그냥버튼은 해당 버튼의 이벤트 리스너에 click으로 추가되어 있고, 해당 핸들러를 클릭해 보면 앞서 작성한 click 함수를 가리킨다.

리액트 버튼은 <botton>의 onClick 이벤트에 noop이라는 핸들러가 추가된다. noop은 no operation 아무런 일도 하지 않는 것을 알 수 있다.

하지만 두 버튼이 동일하게 작동하는데 리액트에서는 이벤트 처리를 어떻게 하는걸까

리액트는 이벤트 핸들러를 해당 이벤트 핸들러를 추가한 각각의 DOM 요소에 부탁하는 것이 아닌, 이벤트 타입(click, change)당 하나의 핸들러를 루트에 부탁한다. 이것을 이벤트 위임이라고 한다. 이벤트 위임 단계의 구성은 다음과 같다.

  • 캡처: 이벤트 핸들러가 트리 최상단 요소에서부터 시작해서 실제 이벤트가 발생한 타깃 요소까지 내려가는 것
  • 타깃: 이벤트 핸들러가 타깃 노드에 도달하는 단계. 여기서 이벤트가 호출된다.
  • 버블링: 이벤트가 발생한 요소에서부터 시작해 최상위 요소까지 다시 올라간다.

다시말해, 이벤트 위임이란 이러한 이벤트 단계의 원리를 활용해 이벤트를 상위 컴포넌트에만 붙이는 것을 의미한다.

ex) 여러개의 li 태그 요소에 이벤트가 필요하다면 ul 태그에만 추가해 이벤트를 위임하는 것

 

정리하면, 리액트 16까지는 이벤트 위임을 각 요소가 아닌 document에서 수행되고 있었다. 하지만 리액트 17부터는 이러한 이벤트 위임이 모두 리액트 컴포넌트 최상단 트리, 즉 루트 요소로 바뀌었다.

이유는 점진적인 업그레이드 지원, 다른 바닐라 자바스크립트 코드 또는 jQuery 등 혼재돼 있는 경우의 혼란을 방지하기 위해서다.

 

이벤트 위임 방식 변경을 도식화한 그림

 

예제 670p 

위 예제에서는 리액트 16에서는 모든 이베트가 document에 달려 있어 stopPropagation이 의미가 없고 리액트 17의 경우 컴포넌트 루트에 달려 있어 document에 부착한 console 이벤트를 볼 수 없다. 이렇게 이벤트가 전파되지 않는 경우가 있는지 확인이 필요하다.

 

10.1.3 import React from 'react'가 더 이상 필요 없다: 새로운 JSX transform

JSX를 실행하기 위해서는 일반적인 자바스크립트를 변환하는 과정이 꼭 필요하다.

리액트 16 버전까지는 React를 사용하는 구문이 없어도 import React from 'react'를 꼭 넣어야 했다.

리액트 17부터는 바벨과 협력해 생략이 가능하다. JSX를 변환할 때 필요한 모듈인 react/jsx-runtime을 불러오는 require 구문도 같이 추가 되기 때문이다.

npx react-codemod update-react-imports

이 명령어로 import React를 모두 삭제할 수 있다.

또한 ESlint를 활용한 정적 분석 방식으로도 import React를 방지할 수 있다.

 

10.1.4 그 밖의 주요 변경 사항

 

이벤트 풀링 제거

리액트 16에서는 이벤트 풀링이라는 기능이 있었는데 이는 이벤트를 처리하기 위한 SyntheticEvent라는 이벤트가 브라우저의 기본 이벤트를 한번 더 감싼 이벤트 객체다. 이벤트 풀링이란 SyntheticEvent 풀을 만들어 이벤트가 발생할 때마다 가져오는 것이다.

  • 이벤트 핸들러가 이벤트를 발생시킨다.
  • 합성 이벤트 풀에서 합성 이벤트 객체에 대한 참조를 가져온다.
  • 이 이벤트 정보를 합성 이벤트 객체에 넣어준다.
  • 유저가 지정한 이벤트 리스너가 실행된다.
  • 이벤트 객체가 초기화되고 다시 이벤트 풀로 돌아간다.

이 이벤트를 사용한 예시를 보면 한번 이벤트 핸들러를 호출한 SyntheticEvent는 이후 재사용을 위해 null로 초기화된다. 따라서 비동기 코드 내부에서 SyntheticEvent인 e에 접근하면 이미 사용되고 초기화된 이후이기 때문에 null 만 얻게 된다. 이 때문에 비동기 코드 내부에서 e에 접근하려면 추가로 e.persist() 같은 처리가 필요하다.

 

이렇게 별도 메모리 공간에 합성 이벤트 객체를 할당해야 한다는 점, 브라우저 성능 향상에 크게 도움이 안된다는 점 때문에 이벤트 풀링 개념은 삭제됐다.

 

useEffect 클린업 함수의 비동기 실행

리액트 16까지는 useEffect에 있는 클린업 함수가 동기적으로 처리됐다. 클린업 함수가 완료되기 전가지는 다른 작업을 방해해 불필요한 성능 저하로 이어지는 문제가 있었다. 버전 17에서부터는 화면이 완전히 업데이트된 이후에 클린업 함수가 비동기적으로 실행된다.

 

컴포넌트의 undefined 반환에 대한 일관적인 처리

리액트 16과 17버전은 컴포넌트 내부에서 undefined를 반환하면 오류가 발생했다. 그러나 16에서 forwardRef나 memo에서 undefined를 반환하면 에러가 발생하지 않는 문제가 있었다.

그래서 17부터는 에러가 정상적으로 발생하게 되었다. 참고로 리액트 18부터는 undefined를 반환해도 에러가 발생하지 않는다.

 

10.2 리액트 18 버전 살펴보기

18버전의 가장 큰 변경점은 바로 동시성 지원이다.

 

10.2.1 새로 추가된 훅 살펴보기

useId

컴포넌트별로 유니크한 값을 생성하는 새로운 훅이다. 

 

컴포넌트가 서버 사이드에서 렌더링되어 클라이언트에 제공되는 예시를 들어보면 679p

하이드레이션 에러가 뜨는데 서버에서는 렌더링했을 때의 Math.random() 값과 클라이언트에서 해당 결과물을 받고 이벤트를 입히기 위한 하이드레이션을 했을 때의 Math.random() 값이 다르기 때문이다.

 

새로운 훅인 useId를 사용하면 클라이언트와 서버에서 불일치를 피하면서 컴포넌트 내부의 고유한 값을 생성할 수 있게 된다.

같은 컴포넌트임에도 서로 인스턴스가 다르면 다른 랜덤한 값을 만들어 내며, 이 값들이 모두 유니크한 것을 볼 수 있다. 또한 서버 사이드와 클라이언트 간에 동일한 값이 생성되어 하이드레이션 이슈도 발생하지 않는다.

useId가 생성하는 값은 :로 감싸져 있는데 이는 CSS 선택자나 querySelector에서 작동하지 않도록 하기 위한 의도적 결과다.

 

useId를 활용한 아이디 생성 알고리즘은 기본적으로 현재 트리에서의 자신의 위치를 나타내는 32글자의 이진 문자열로 이뤄져 있고 왼쪽 5자리가 부모의 트리를 나타낸다. 앞글자가 R이면 서버에서 생성된 값, r이면 클라이언트에서 생성된 값이다. 

 

useTransition 

UI 변경을 가로막지 않고 상태를 업데이트할 수 있는 리액트 훅이다. 상태 업데이트를 긴급하지 않은 것으로 간주해 무거운 렌더링 작업을 조금 미룰 수 있어 좀더 나은 사용자 경험을 제공한다.

컴포넌트에서만 사용 가능하며 훅을 사용할 수 없는 상황에선 startTransition을 바로 import해서 사용하면 된다.

앞서 말한 리액트 18의 변경사항의 핵심 중 하나인 동시성을 다룰 수 있는 훅으로 느린 렌더링 과정에서 로딩 화면을 보여주거나 지금 진행 중인 렌더링을 버리고 새로운 상태 값으로 다시 렌더링하는 등의 작업을 할 수 있게 된다.

 

사용 시 주의할 점

  • startTransition 내부는 반드시 setState와 같은 상태를 업데이트하는 함수와 관련된 작업만 넘길 수 있다. 만약 props나 사용자 정의 훅에서 반환하는 값 등을 사용한다면 useDefferedValue를 사용해야 한다.
  • startTransition으로 넘겨주는 상태 업데이트는 다른 모든 동기 상태 업데이트로 인해 실행이 지연될 수 있다.
  • startTransition으로 넘겨주는 함수는 반드시 동기 함수여야 한다. 

useDefferedValue 

리액트 컴포넌트 트리에서 리렌더링이 급하지 않은 부분을 지연할 수 있게 도와주는 훅이다.

특정 시간 동안 발생하는 이벤트를 하나로 인식해 한번만 실행하게 해주는 디바운스는 고정된 지연 시간이 필요하지만,

useDefferedValue는 고정된 지연 시간 없이 첫 번째 렌더링이 완료된 후 useDefferedValue로 지연된 렌더링을 수행한다. 그러므로 이 지연된 렌더링은 중단할 수도 있으며, 사용자의 인터렉션을 차단하지도 않는다.

 

useDefferedValue와 useTransition의 차이점

useTransition은 state 값을 업데이트하는 함수를 감싸서 사용한다.

useDefferedValue는 state 값 자체만을 감싸서 사용한다.

 

낮은 순위로 처리해야 할 작업에 대해 직접적으로 상태를 업데이트할 수 있는 코드에 접근할 수 있다면 useTransition 추천,

컴포넌트의 props와 같이 상태 업데이트에 관여할 수 없고 오로지 값만 받아야 하는 상황이면 useDefferedValue 추천

 

useSyncExternalStore

먼저 '테어링(tearing)'에 대한 이해가 필요하다.

테어링(tearing)은 영어로 '찢어진다'란 뜻인데 리액트에서는 하나의 state 값이 있음에도 서로 다른 값(보통 state나 props의 이전과 이후)을 기준으로 렌더링되는 현상을 말한다.

 

리액트 18에서 useTransition, useDefferedValue의 훅처럼 렌더링을 일시 중지하거나 뒤로 미루는 등의 최적화가 가능해지면서 동시성 이슈가 발생할 수 있다. 

 

 

그림의 순서를 살펴보면,

  1. 첫 번째 컴포넌트에서는 외부 데이터 스토어의 값이 파란색이었으므로 파란색을 렌더링한다.
  2. 나머지 컴포넌트들도 파란색으로 렌더링을 준비하고 있었다.
  3. 갑자기 외부 데이터 스토어의 값이 빨간색으로 변경됐다.
  4. 나머지 컴포넌트들은 렌더링 도중에 바뀐 색을 확인해 빨간색으로 렌더링했다.
  5. 결과적으로 같은 데이터 소스를 바라보고 있음에도 컴포넌트의 색상이 달라지는 테어링 현상이 발생했다.

리액트가 관리할 수 없는 외부 데이터 소스(useState나 useReducer가 아닌 모든 것들)에서 추구하는 동시성 처리가 추가돼 있지 않으면 이처럼 테어링 현상이 발생할 수 있다. 이 문제를 해결하기 위한 훅이 useSyncExternalStore다. 

 

import { useSyncExternalStore } from 'react'

useSyncExternalStore(
	subscribe: (callback) => Unsubscribe
	getSnapshot: () => State
) => State

 

  • 첫 번째 인수는 subscribe로, 콜백함수를 받아 스토어에 등록하는 용도로 사용된다. 스토어에 있는 값이 변경되면 이 콜백이 호출돼야 한다. useSyncExternalStore는 이 훅을 사용하는 컴포넌트를 리렌더링한다.
  • 두 번째 인수는 컴포넌트에 필요한 현재 스토어의 데이터를 반환하는 함수다. 이 함수는 스토어가 변경되지 않았다면 매번 함수를 호출할 때마다 동일한 값을 반환해야 한다. 스토어에서 값이 변경됐다면 이 값을 이전 값과 Object.is로 비교해 정말로 값이 변경됐다면 컴포넌트를 리렌더링 한다.
  • 마지막 인수는 옵셔널 값으로, 서버 사이드 렌더링 시에 내부 리액트를 하이드레이션하는 도중에만 사용된다. 서버 사이드에서 렌더링되는 훅이라면 반드시 이 값을 넘겨줘야 하며, 클라이언트의 값과 불일치가 발생할 경우 오류가 발생한다.

사용중인 관리 라이브러리가 외부에서 상태를 관리하고 있다면 useSyncExternalStore를 통해 외부 데이터 소스의 변경을 추적하고 있는지 확인해야 한다.

 

useInsertionEffect

CSS-in-js 라이브러리를 위한 훅이다.

CSS의 추가 및 수정은 브라우저에서 렌더링하는 작업 대부분을 다시 계산해 작업해야 하므로 모든 컴포넌트에 영향을 미칠 수 있어 매우 무거운 작업이다. 그래서 리액트 17버전과 styled-components에서는 클라이언트 렌더링 시 이러한 작업이 발생하지 않도록 서버 사이드에서 스타일 코드를 삽입했다. 그러나 이 작업을 훅으로 처리하기가 어려웠는데 훅에서 이러한 작업을 할 수 있도록 도와주는 것이 바로 useInsertionEffect다.

 

기본적인 구조는 useEffect와 동일하지만 실행 시점이 다른데 useInsertionEffect는 DOM이 실제로 변경되기 전에 동기적으로 실행된다. 이 훅 내부에 스타일을 삽입하는 코드를 집어넣음으로써 브라우저가 레이아웃을 계산하기 전에 실행될 수 있게 한다. 

 

실행되는 순서는 useInsertionEffect -> useLayoutEffect -> useEffect 이다.

useLayoutEffect는 모든 DOM의 변경 작업이 다 끝난 이후에 실행되는 반면 useInsertionEffect는 이러한 DOM 변경 작업 이전에 실행된다. 이는 브라우저가 다시 스타일을 입혀서 DOM을 재계산하지 않아도 된다는 점에서 매우 큰 차이다.

라이브러리를 작성하는 경우가 아니라면 가급적 사용하지 않는 것이 좋다.

 

10.2.2 react-dom/client
리액트 18이하 버전에서 만든 CRA으로 프로젝트를 유지보수 중이라면 18로 업그레이드 할때 반드시 index.{t|j}jsx에 있는 내용을 변경해야 한다.

 

createRoot

기존의 reatc-dom에 있던 render 메서드를 대체할 새로운 메서드다. createRoot와 render를 함께 사용해야 한다.

//before
import ReactDOM from 'react-dom'
import App from 'App'

const container = document.getElementById('root')

ReactDOM.render(<App />, container)

//after
import ReactDOM from 'react-dom'
import App from 'App'

const container = document.getElementById('root')

const root = ReactDOM.createRoot(container)
root.render(<App />)

 

hydrateRoot

서버 사이드 렌더링 애플리케이션에서 하이드레이션을 하기 위한 새로운 메서드다. ReactDOM 서버 API와 함께 사용된다.

//before
import ReactDOM from 'react-dom'
import App from 'App'

const container = document.getElementById('root')

ReactDOM.hydrate(<App />, container)

//after
import ReactDOM from 'react-dom'
import App from 'App'

const container = document.getElementById('root')

const root = ReactDOM.hydrateRoot(container, <App />)

 

두 API는 새로운 옵션인 onRecoverableError를 인수로 받는다. 이 옵션은 리액트가 렌더링 또는 하이드레이션 과정에서 에러가 발생했을 때 실행하는 콜백 함수다.

 

10.2.3 react-dom/server

 

renderToPipeableStream

리액트 컴포넌트를 HTML로 렌더링하는 메서드다. 스트림을 지원하는데 HTML을 점진적으로 렌더링하고 클라이언트에서는 중간에 script를 삽입하는 등의 작업을 할 수 있다. 이를 통해 서버에서는 Suspense를 사용해 빠르게 렌더링이 필요한 부분을 먼저 렌더링할 수 있고, 값비싼 연산으로 구성된 부분은 이후에 렌더링되게 한다. 

위의 hydrateRoot를 호출하면 서버에서는 HTML을 렌더링하고, 클라이언트의 리액트에서는 여기에 이벤트만 추가함으로써 첫 번째 로딩을 매우 빠르게 수행할 수 있다.

 

이전의 renderToNodeStream은 무조건 렌더링을 순서대로 해야 해서 렌더링 중간에 오래 걸리는 작업이 있다면 나머지 렌더링도 덩달아 지연되는 문제가 있었는데 renderToPipeableStream을 활용하면 순서나 오래 걸리는 렌더링에 영향 받지 않아 빠르게 렌더링을 수행할 수 있다.

 

renderToReadableStream

renderToPipeableStream이 Node.js 환경에서의 렌더링을 위해 사용된다면, renderToReadableStream은 웹 스트림을 기반으로 작동한다. 서버 환경이 아닌 클라우드플레어나 디노같은 웹 스트림을 사용하던 모던 엣지 런타임 환경에서 사용되는 메서드다. 사용할 일은 거의 없다.

 

10.2.4 자동 배치(Automatic Batching)

리액트가 여러 상태 업데이트를 하나의 리렌더링으로 묶어서 성능을 향상시키는 방법이다. 

버튼 클릭 한 번에 두 개 이상의 state를 동시에 업데이트한다고 가정할 때 자동 배치에서는 이를 하나의 렌더링으로 묶어서 수행할 수 있다. 예제 698p 를 보면,

리액트 18 버전에서는 한 번의 rendered만 기록되고 17 버전은 두번 기록되는 것을 확인할 수 있다. 이는 17 이하의 버전의 경우 이벤트 핸들러 내부에서 이러한 자동 배치 작업이 이뤄지고 있었지만 Promise, setTimeout 같은 비동기 이벤트에서는 자동배치가 이뤄지지 않아서이다. 이를 위해 18부터는 루트 컴포넌트를 createRoot를 사용해서 만들어 모든 업데이트가 배치 작업으로 최적화할 수 있게 됐다.

 

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

const rootElement = document.getElementById('root')
const root = ReactDOM.createRoot(rootElement)

root.render(
	<React.StrictMode>
		<App />
	</React.StrictMode>
)

 

이렇게 하면 자동배치가 활성화되어 리액트가 동기, 비동기, 이벤트 핸들러 등 관계 없이 렌더링을 배치로 수행한다.

만약 위와 같은 자동 배치를 하고 싶지 않다면 flushSync를 사용하면 된다. flushSync는 react가 아닌 react-dom에서 제공된다.

import { flushSync } from 'react-dom'

function handleClick() {
	flushSync(()=>{
		setCounter((c)=>c+1)
	})
	flushSync(()=>{
		setFlag((f)=>!f)
	})
}

 

10.2.5 더욱 엄격해진 엄격 모드

리액트의 엄격 모드

리액트에서 제공하는 컴포넌트 중 하나로, 잠재적인 버그를 찾는 데 도움을 주며 개발자 모드에서만 작동한다.

 

더 이상 안전하지 않은 특정 생명주기를 사용하는 컴포넌트에 대한 경고 

클래스 컴포넌트에서 사용되는 생명주기 메서드 중 일부인 componentWillMount, componentWillReceiveProps, componentWillUpdate는 사용할 수 없다. 16.3버전부터 UNSAFE_가 붙게 됐고, 붙지 않는 생명주기 메서드를 사용하면 경고 로그가 기록된다. 17버전부터는 UNSAFE_가 붙은 생명주기 메서드만 남았다.

그럼에도 UNSAFE_가 붙은 생명주기 메서드를 사용하면서 엄격모드를 키면 "Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code ... 생략" 에러가 뜬다. (그래서 어쩌라는거임...?)

문자열 ref 사용 금지

과거 리액트에서는 레거시 문자열 ref라 해서 createRef가 없어도 컴포넌트 내부에서 문자열로 ref를 생성하고, 이를 사용해 DOM 노드를 참조하는 것이 가능했다.

class UnsafeClassComponent extends Component {
 componentDidMount() {
  //'refs' is deprecated.
  // <input type="text" />
  console.log(this.refs.myInput)
}

 render() {
  return (
   <div>
   	<input type="text" ref="myInput" />
   </div>  
  )
 }
}

render()에 있는 ref를 보면 단순히 myInput이라는 문자열로 ref에 할당한 것을 볼 수 있으며, 이를 토대로 refs를 바탕으로 DOM에 접근할 수 있다는 것을 알 수 있다.

그러나 다음과 같은 문제도 염두해야 한다. 

  • 문자열로 값을 주는 것은 여러 컴포넌트에 걸쳐 사용 될 수 있으므로 충돌의 여지가 있다.
  • 단순히 문자열로만 존재하기 때문에 실제로 어떤 ref에서 참조되고 있는지 파악이 어렵다.
  • 리액트가 계속해서 현재 렌더링되고 있는 컴포넌트의 ref의 값을 추적해야 하기 때문에 성능 이슈가 있다.

findDOMNode에 대한 경고 출력

클래스 컴포넌트 인스턴스에서 실제 DOM 요소에 대한 참조를 가져올 수 있는, 현재는 사용하지 않는 것을 권장하는 메서드다.

예제 703p 에서 ReactDOM에서 제공하는 findDOMNode() 메서드를 활용해 클래스 컴포넌트의 요소에 직접 접근해 해당 DOM 요소의 스타일을 수정한 것을 확인할 수 있다. 이 역시 엄격 모드에서 사용하면 에러가 발생한다.

이 메서드 또한 다음과 같은 문제가 있다.

  • 부모가 특정 자식만 별도로 렌더링하는 것이 가능한데 이는 리액트가 추구하는 트리 추상화 구조를 무너뜨린다.
  • 항상 첫 번째 자식을 반환하는데 이는 Fragement를 사용할 때 어색해진다.
  • 일회성 API라는 특징 때문에 자식 컴포넌트가 특정 시점에서 다른 노드를 렌더링할 경우 이러한 변경 사항을 추적할 수 없다.
  • 따라서 문자열 ref와 마찬가지로 createRef, useRef를 사용하는 방향으로 전환되었다. 

구 Context API 사용 시 발생하는 경고

childContextTypes와 getChildContext를 사용하는 구 리액트 Context API를 사용하면 엄격 모드에서 에러가 발생하는 것을 확인할 수 있다.

 

예상치 못한 부작용(side-effects) 검사

엄격모드 내부에서는 다음 내용을 의도적으로 이중 호출한다.

  • 클래스 컴포넌트의 constructor, render, shouldComponentUpdate, getDerivedStateFromProps
  • 클래스 컴포넌트의 setState의 첫 번째 인수
  • 함수 컴포넌트의 body
  • useState, useMemo, useReducer에 전달되는 함수

엄격모드에서 두 번씩 실행되는 이유

  • 함수형 프로그래밍의 원칙에 따라 리액트의 모든 컴포넌트는 항상 순수하다고 가정하기 때문
  • 실제로 항상 순수한 결과물을 내고 있는지 개발자에게 확인시켜 주기 위함

console.log 사용 시 17버전에서는 혼선 방지를 위해 의도적으로 두번씩 기록되지 않게 했고, 18번전에서는 두번씩 기록하되 두번째 기록은 회색으로 표시되게 바뀌었다.

 

리액트 18에서 추가된 엄격 모드

향후 리액트에서 컴포넌트가 마운트 해제된 상태에서도 컴포넌트 내부의 상태값을 유지할 수 있는 기능을 제공할 예정이라고 했는데 이를 위해 엄격모드의 개발모드에 새로운 기능이 도입됐다.

컴포넌트가 최초의 마운트될 때 자동으로 모든 컴포넌트를 마운트 해제하고 두번째 마운트에서 이전 상태를 복원하게 된다. 

예제 708p를 보면 실행결과가 18버전에서는 useEffect가 두 번 작동한 것처럼 보이는데 이는 앞서 말한 기능을 위해 18버전의 StrictMode에서 의도된 작동 방식이다. 따라서 클린업 함수를 사용하는 것이 좋다. 

 

10.2.6 Suspense 기능 강화

컴포넌트를 동적으로 가져올 수 있게 도와주는 기능이다.

//Sample Component
export default function SampleComponent() {
 return <>동적으로 가져오는 컴포넌트</>
}

//app.tsx
import { Suspense, lazy } from 'react'

const DynamicSampleComponent = lazy(()=>import('./SampleComponent'))

export default function App() {
 return (
  <Suspense>
  	<DynamicSampleComponent/>
  </Suspense>
 )
}

 

  • app.tsx에서 SampleComponent를 React.lazy를 통해 불러온다.
  • React.lazy는 컴포넌트를 첫 번째 렌더링 시에 불러오지 않고, 최초 렌더링 이후에 컴포넌트를 지연시켜 불러오는 역할을 한다.
  • Suspense는 React.lazy를 통해 지연시켜 불러온 컴포넌트를 미처 불러오지 못했을 때 보여주는 fallback을 나타낸다.
  • children으로는 React.lazy로 선언한 지연 컴포넌트를 받는다. 지연 컴포넌트를 로딩하기 전에는 fallback을 보여주고, 이 lazy로 불러온 컴포넌트가 지연 로딩이 완료되면 fallback 대신 비로소 해당 컴포넌트를 보여주게 된다.
  • 이를 통해 lazy와 Suspense는 한 쌍으로 사용됐고, 애플리케이션에서 상대적으로 중요하지 않은 컴포넌트를 분할해 초기 렌더링 속도를 향상시키는 데 많은 도움이 된다.

18 이전의 문제점

  • 컴포넌트가 아직 보이기도 전에 useEffect가 실행되는 문제
  • 서버에서 사용할 수 없는 문제, Suspense를 서버 사이드 렌더링 구조에서 사용하려면 useMount와 같은 훅으로 구현해 클리이언트에서만 작동하도록 해야했다.

18 버전에서의 Suspense 변경 내용

  • 아직 마운트되기 직전임에도 effect가 빠르게 실행되는 문제 수정되어 컴포넌트가 실제로 화면에 노출될 때 effect가 실행된다.
  • Suspense로 인해 노출이 된다면 useLayoutEffect의 effect(componentDidMount)가, 가려진다면 useLayoutEffect의 cleanUp(componentWillUnmount)이 정상적으로 실행된다.
  • Suspense를 서버에서도 실행할 수 있게 된다. 서버에서 일단 fallback 상태의 트리를 클라이언트에 제공하고, 불러올 준비가 된다면 렌더링된다.
  • Suspense 내에 스크롤링이 추가됐다. 화면이 너무 자주 업데이트되어 시각적으로 방해 받는 것을 방지하기 위해 다음 렌더링을 보여주기 전 잠시 대기한다. 즉 중첩된 Suspense의 fallback이 있다면 자동으로 스크롤되어 최대한 자연스럽게 보여준다.

10.2.7 인터넷 익스플로러 지원 중단에 따른 추가 폴리필 필요

리액트는 리액트를 사용하는 코드에서 최신 자바스크립트 기능을 사용할 수 있다는 가정 하에 배포된다.

이러한 기능을 지원하지 않는 브라우저에서 서비스해야 한다면 폴리필을 추가해야 한다.

 

10.2.8 그 밖에 알아두면 좋은 변경사항

  • undefined를 반환해도 에러가 발생하지 않고 null 반환과 동일하게 처리된다.
  • <Suspense fallback={undefined}>도 null과 동일하게 처리된다.
  • renderToNodeStream이 지원 중단됐다. 대산 renderToPipeableStream을 사용하는 것이 권장된다.