본문 바로가기

Study

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

17,18 버전의 변경사항을 알아야 하는 이유?

현재 리액트 16버전을 사용하는 사이트가 가장 많다.

그러나 리액트가 제공하는 최신 기능을 모두 활용하려면 새로운 버전들도 조금씩 따라잡을 필요가 있다.

또한 리액트에 의존적인 라이브러리를 사용한다면 peerDependencies를 통해 리액트에 의존하고 있으므로 버전 업을 위해 이 라이브러리가 지원하는 리액트 버전에 대해 꼼꼼히 살펴봐야 한다.

리액트 17버전 살펴보기

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

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

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

그러나 리액트 17버전부터는 점진적인 업그레이드를 지원한다.

💡 점진적인 업그레이드?
전체 애플리케이션 트리는 리액트 17이지만 일부 트리와 컴포넌트에 대해서만 리액트 18을 선택할 수 있는 것을 의미한다. 한번에 업그레이드가 불가능 한 큰 프로젝트의 경우에 이런 점진적 업그레이드 방식을 사용해 조금씩 업데이트 할 수 있다.

2. 이벤트 위임 방식의 변경

리액트는 이벤트 핸들러를 해당 이벤트를 추가한 각각의 DOM 요소에 부탁하는 것이 아니라, 이벤트 타입당 하나의 핸들러를 루트에 부착하는 이벤트 위임 방식을 사용한다.

💡 이벤트 위임이란?
특정 노드에 일일이 이벤트 리스너를 추가하는 대신, 이벤트 리스너를 특정 노드들을 포함하는 상위 노드에 연결하여 이벤트를 전파하는 것

💡 이벤트 위임의 단계
캡처(capture): 이벤트 핸들러가 최상단 요소부터 시작해 이벤트 타겟까지 내려가는것
타깃(target): 이벤트 핸들러가 타깃 노드에 도달하는 단계버블링(bubbling): 이벤트가 발생한 요소부터 시작해 최상위 요소까지 다시 올라가는 것
이벤트가 전파되는 방식의 기본 값은 버블링(bubbling)이며, 캡처링(capturing) 방식으로 변경해 사용할 수도 있다.

 

이벤트 위임을 사용하는 이유?

⇒ 이벤트를 하나의 요소에서 관리하기 때문에 관리 포인트가 줄어 효율적으로 관리가 가능

이러한 이유로 리액트에서는 이벤트 위임을 적극적으로 사용했는데,

17버전에서는 이벤트 위임이 모두 document가 아닌 리액트 컴포넌트 최상단 트리인 루트 요소에 부착되도록 변경되었다.

17버전에서는 이벤트가 루트인 div#root에 부착

위임 방식이 변경된 이유?

아래 같은 경우에 document에 이벤트 리스너가 부착되게되면 버블링을 막는 stopPropagation가 안먹히는 등의 문제가 생겨 컴포넌트의 최상위 부착으로 변경되었다.

  • HTML에서 여러 버전의 React가 공존하며 DOM에 앱을 생성하는 경우
  • 다른 기술로 빌드 된 앱 일부에 React를 사용해 적용할 경우

이러한 수정으로 이벤트는 해당 리액트 컴포넌트 트리 수준으로 격리되므로 이벤트 버블링으로 인한 혼선을 방지 할 수 있다.

3. 새로운 JSX transform

리액트에서 사용되는 JSX 문법은 브라우저가 이해할 수 있는 코드가 아니므로 자바스크립트로 변환하는 과정이 필요하다.

16버전까지는 이런 변환을 위해 코드 내에 React 사용 구문이 없어도 import React from ‘react’가 필요했다.

그러나 17버전부터는 바벨과 협력해 이러한 import 구문 없이도 JSX를 변환할 수 있게 되었다.

만약 16 → 17로 마이그레이션하면서 이 구문을 제거하고 싶다면 다음 명령어를 사용해 모두 제거 가능하다.

npx react-codemod update-react-imports

4. 그 밖의 주요 변경 사항

이벤트 풀링 제거

💡 이벤트 풀링이란?
SyntheticEvent 풀을 만들어서 이벤트가 발생할 때마다 이벤트 객체를 가져오는 것

비동기 코드로 이벤트 핸들러에 접근하기 위해서 이런 별도 메모리 공간에 합성 이벤트 객체로 할당해야 한다는 점과 모던 브라우저에서는 이런 방식이 성능향상에 크게 도움이 안되는 점으로 이벤트 풀링 개념이 삭제되었다.

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

클린업 함수는 리액트 16버전까지는 동기적으로 처리되었으나 17부터는 화면이 업데이트가 완전히 끝난 이후에 실행되도록 바뀌었다.

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

리액트 16,17 버전은 컴포넌트 내부에서 undefined를 반환하면 오류가 발생했으나

18부터는 undefined를 반환해도 에러가 발생하지 않는다.

리액트 18버전 살펴보기

리액트 17버전에서는 점진적인 업그레이드를 위한 준비였다면 18버전은 다양한 기능이 추가되었다.

1. 새로 추가된 훅 살펴보기

useId

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

useTransition

UI 변경을 가로막지 않고 상태를 업데이트 할 수 있는 리액트 훅이다. 리액트 18의 변경 사항의 핵심 중 하나인 ‘동시성’을 다룰 수 있는 새로운 훅이다. 이를 활용하면 무거운 렌더링 작업을 조금 미루어 사용자에게 더 나은 사용자 경험을 제공할 수 있다.

// 상태 업데이트가 진행중인지를 확인하는 boolean 값인 isPending과
// 긴급하지 않은 상태 업데이트로 간주할 set 함수를 넣어두는 함수인 startTransition을 인수로 받음.
import {useTransition} from 'react'

const [isPending, startTransition] = useTransition()

 

사용시 주의 할 점

  • startTransition 내부는 반드시 setState와 같은 상태를 업데이트 하는 함수와 관련된 작업만 넘길 수 있다.
  • startTransition 넘겨주는 상태 업데이트는 다른 모든 동기 상태 업데이트로 인해 실행이 지연될 수 있다.
  • startTransition으로 넘겨주는 함수는 반드시 동기 함수여야 한다.

useDeferredValue

컴포넌트 트리에서 리렌더링이 급하지 않은 부분을 지연할 수 있게 도와주는 훅이다. useDeferredValue는 고정된 지연 시간 없이 첫번째 렌더링이 완료된 이후에 이 useDeferredValue로 지연된 렌더링을 수행한다.

useTransition은 state 값을 업데이트 하는 함수를 감싸서 사용하는 반면에 useDefferedValue는 state 값 자체만을 감싸서 사용한다.

직접적으로 상태를 업데이트 할 수 있는 코드에 접근 할 수 있다면 useTransition을 사용하고 상태 업데이트에 관여 할 수는 없고 값만 받아야 한다면 useDeferredValue를 사용하는 것이 좋다.

useSyncExternalStore

리액트 18부터는 리액트가 렌더링을 중지했다가 다시 실행하는 등 ‘양보’가 가능해져 테어링(tearing) 현상이 나타날 가능성이 존재한다.

리액트에서 관리하는 state라면 내부적으로 이러한 문제를 해결 하기 위한 처리를 해두었지만, 외부 데이터 소스에 리액트에서 추구하는 동시성 처리가 추가되어 있지 않다면 테어링 현상이 발생할 수 있다.

이를 해결하기 위한 훅이 useSyncExternalStore 이다.

import {useSyncExternalStore} from 'react'

// useSyncExternalStore(
//	subscribe: (callback) => Unsubscribe
//	getSnapShot: () => state
//) => State
  • 첫번째 인수는 콜백 함수를 받아 스토어에 등록하는 용도로 사용한다.
  • 두번째 인수는 컴포넌트에 필요한 현재 스토어의 데이터를 반환하는 함수다. 스토에어서 값이 변경되었다면 이 값을 이전 값과 Object.is로 비교해 정말 값이 변경됐다면 컴포넌트를 리렌더링한다.
  • 마지막 인수는 옵셔널 값으로 서버 사이드 렌더링 시에 내부 리액트를 하이드레이션하는 도중에만 사용한다.

이 훅은 애플리케이션 코드에 직접적으로 사용할 일은 많지 않지만 사용중인 관리 라이브러리가 외부에서 상태를 관리하고 있다면 해당 훅을 통해 외부 데이터 소스의 변경을 추적하고 있는지 반드시 확인해야 한다.

useInsertionEffect

CSS-in-js 라이브러리를 위한 훅으로 이 훅 내부에 스타일을 삽입하는 코드를 집어 넣음으로써 브라우저가 레이아웃을 계산하기 전에 실행될 수 있게끔 자연스러운 스타일 삽입을 가능하게 해주는 훅이다.

react-dom/client

클라이언트에서 리액트 트리를 만들때 사용되는 API가 변경되었다.

  1. createRoot 기존에 있던 render 메서드를 대체할 새로운 메서드이다.
  2. hydrateRoot 서버 사이드 렌더링 어플리케이션에서 하이드레이션을 하기 위한 새로운 메서드이다.

react-dom/server

서버에서도 컴포넌트를 생성하는 API에 변경이 있었다.

  1. renderToPipeableStream 리액트 컴포넌트를 HTML로 렌더링하는 메서드이다. 스트림을 지원하는 메서드로 Suspense를 활용해 빠르게 렌더링이 필요한 부분을 먼저 렌더링 하고 오래걸리는 연산은 이후에 렌더링 할 수 있게끔 해준다.
  2. renderToReadableStream
  3. renderToPipeableStream이 Node.js 환경에서의 렌더링을 위해 사용된다면, renderToReadableStream은 웹스트림을 기반으로 작동한다.

2. 자동배치(Automatic Batching)

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

리액트 17이하의 과거 버전의 경우 이벤트 핸들러 내부에서는 이러한 자동배치 작업이 이뤄졌지만

setTimeout, Promise 같은 비동기 이벤트에서는 자동 배치가 이뤄지고 있지 않았다.

즉, 동기와 비동기 작업에 일관성이 없었고 이를 보완하기 위해 리액트 18버전부터는 모든 업데이트가 배치 작업으로 최적화 할 수 있게 되었다.

자동배치를 하고싶지 않으면 flushSync를 사용하면 된다.

3. 더욱 엄격해진 엄격모드

먼저, 리액트에서의 엄격모드가 무엇일까?

import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client'

const root = createRoot(document.getElementId('root'))

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

위 처럼 컴포넌트 형태로 사용 가능한 엄격모드는 리액트 애플리케이션에서 발생할 수도 있는 잠재적인 버그를 찾는데 도움이 되는 컴포넌트이다.

 

엄격모드에서 하는 작업

  • 더 이상 안전하지 않는 특정 생명주기를 사용하는 컴포넌트에 대한 경고
  • 문자열 ref 사용 금지
  • findDOMNode에 대한 경고 출력
  • 구 Context API 사용 시 발생하는 경고
  • 예상치 못한 부작용(side-effects) 검사

엄격모드에서는 위에서 언급한 내용이 실제로 지켜지고 있는지를 검사한다.

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

향후 리액트에서는 컴포넌트가 마운트 해제된 상태에서도 컴포넌트 내부의 상태값을 유지할 수 있는 기능을 제공할 예정이라고 밝혔다.

이러한 변경을 위해 StrictMode에서 고의로 useEffect를 두 번 작동시키는 내용을 추가했다. 이를 위해 useEffect를 사용할 때 반드시 적절한 cleanup 함수를 배치해서 반복 실행될 수 있는 useEffect로부터 최대한 자유로운 컴포넌트를 만드는 것이 좋다.

4. Suspense 기능 강화

Suspense는 크게 두개의 인수를 받는데 하나는 fallback props로 지연시켜 불러온 컴포넌트를 미처 불러오지

못했을 때 보어주는 fallback을 나타낸다. 그리고 children으로는 React.lazy로 선언한 지연 컴포넌트를 받는다.

리액트 18버전 Suspense가 실험단계를 벗어나 정식으로 지원된다.

 

변경 사항

  • 마운트 되기 직전임에도 effect가 빠르게 실행되는 문제 수정, 이제 컴포넌트가 실제로 화면에 노출될 때 effect가 실행된다.
  • Suspense를 이제 서버에서도 실행 할 수 있게 된다.
  • Suspense 내에 스로틀링이 추가되어 중첩된 suspense의 fallback이 있다면 자동으로 스로틀되어 최대한 자연스럽게 보여주기 위해 노력한다.

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

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

  • Promise
  • Symbol
  • Object.assign

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

리액트 17의 버전업은 단순히 다음 리액트 버전 업을 조금 더 쉽게 만드는 데 초점을 맞췄다면

리액트 18 버전 업의 핵심은 동시성 렌더링이다. 앞으로 개발할 애플리케이션에 동시성 모드를 염두에 두고 있다면 사용하고자 하는 라이브러리가 이를 완벽하게 지원하는지 반드시 검토가 필요하다.