6주차 스터디
16~17장 · 2개 챕터
이번 주 핵심 요약
16장
-
ErrorBoundary는 렌더링 과정에서 발생하는 에러를 막을 수 있다.
-
클래스 컴포넌트를 사용해서 에러 바운더리를 막을 수 있으며, 두 메서드가 존재한다.
- static getDerivedStateFromError(error): 렌더링 과정에서 에러가 발생했을 때 호출된다. 에러를 인자로 받는만큼 컴포넌트의 상태를 갱신할 수 있다. 상태 갱신시 컴포넌트 리렌더링이 되고, 이 때 개발자가 준비한 대체(Fallback) UI를 보여줄 수 있다.
- componentDidCatch(error, errorInfo): 에러가 포착된 후 호출된다. 주로 센트리(Sentry)나 데이터독(Datadog) 같은 외부 에러 모니터링 서비스에 에러 정보를 전송하는 등의 부수 효과를 처리하는 데 사용된다.
-
에러는 전파되며, 과정은 이벤트 버블링과 유사하다. 컴포넌트 트리를 따라 상위로 전파되며 에러 발생 지점부터 가장 가까운 부모 에러 바운더리를 찾아가는 과정을 에러 전파라고 부른다. 당연히 에러 바운더리에서 막히면 더 이상은 전파되지 않는다.
-
에러 바운더리는 리액트 렌더링 생명주기 동안 발생하는 에러만 감지할 수 있다. 이벤트 핸들러 내 에러나 fetch 비동기 에러를 잡을 수 없다는 이야기다.
-
그럼 사용이 너무 제한적이라고 생각할 수 있지만, throw error를 통해 명시적으로 에러를 발생시킨다면 에러 바운더리를 활용할 수 있다. useThrowError 함수를 보자.
-
react-error-boundary를 사용하면 수동적으로 throw 하는 동작 없이도 이벤트 핸들러 등에서 발생한 에러를 잡게 도와준다.
-
Context API는 앞에서도 여러 번 나온 내용이라 기본적인 설명은 생략하겠습니다.
-
다만 Context 상위 부모가 리렌더링 되는 경우에도 Context 내에서 value props로 전달된 객체가 있다면, 주소값이 바뀔 것이고 그걸 바라보는 자식들도 불필요하게 리렌더링 될 것이다.
- 이런 경우 Context의 내부 value에 useMemo hook을 사용하도록 하자. 혹은 Context를 적절히 분리하자.
- 다른 상태 라이브러리와 같이 셀렉터 패턴을 적용해볼까? 하는 마음도 들겠지만 Context API에선 쉽지 않다.
- 고차 컴포넌트나 memo()를 활용해서 최적화 할 수도 있다. (개인적인 생각 : 이런 최적화 기법을 하나 둘 씩 적용하다 보면 zustand와 같은 라이브러리가 될 것 같다. 그냥 이쯤되면 zustand 설치하자.)
-
Suspense는 비동기 작업이 완료될 때까지 UI 렌더링을 선언적으로 일시 중단하고, 이 기간동안 Fallback UI를 보여주는 매커니즘이다.
-
기본적으로 우리는 기존에 주로 React.lazy() API를 사용하며 경험해 보았을 것이다.
-
리액트 팀에서 만든 wrapPromise 코드 예시를 보면 이해가 쉬울 것이다. 기본 로직은 아래와 같다.
- promise를 인자로 받고, status가 pending이거나 error인 경우 각각 suspender, result(error)를 throw 한다.
<Suspense>로 던지는 것이다. - status가 success라면 성공적으로 페칭이 완료된 것이므로 result를 반환한다.
- promise를 인자로 받고, status가 pending이거나 error인 경우 각각 suspender, result(error)를 throw 한다.
-
React 17 에서는 특정 컴포넌트가
<Suspense>에 의해 지연될 때 형제 컴포넌트들의 생명주기 훅들은 실행된 후 Fallback UI에 의해 숨김 처리되었다고 한다. 그로 인해 의도치 않은 동작이 일어나거나 불필요한 리소스를 소모할 수 있었다. -
React 18에서는 createRoot() API를 통해 동시성 기능이 정식으로 도입되면서 서버 사이드에서도 사용 가능하고, 형제 컴포넌트들의 렌더링 및 이펙트 실행도 함께 지연되었다고 한다. (19버전도 한번 알아봐야겠다.)
-
React 18의 동시성 기능이 추가되면서 useTransition(), useDefferedValue() 등과도 사용 가능해서, 활용도가 높아졌다.
-
무엇보다
@tanstack/react-query, @apollo/client등에서도Suspense Query기능을 지원하기에 우리에게 익숙해졌다. -
위에서 설명한대로 우리는
<Suspense>에 pending or error 객체를 던지는데,<Suspense>를<ErrorBoundary>로 감싸면 에러를 캐치할 수 있다. -
use()는 React19에서 들어온 hook으로
Suspense를 통해 선언적으로 로딩 상태를 관리하고, 마치 동기 코드처럼 값을 반환할 수 있다. Context나 Promise를 인자로 받을 수 있는데, 조건문 내부에서 호출될 수 있어 코드를 더 효율적, 직관적으로 만들어준다. 데이터 페칭 시에도 useEffect를 대체할 수 있어 워터폴 현상, 복잡한 상태 관리, 경쟁 상태 등을 해소해준다. 데이터를 가져오면서 동시에 렌더링한다 가 핵심이다. -
use()는 호출 시에 리액트의 내부적인 훅 리스트에 의존하는 것이 아니라 인자로 받은 값에 의존하기에 조건문 안에서도 호출이 가능하다.
17장
-
한 줄 요약: 고급 생명주기 훅, React의 동시성 기능을 활용하여 사용자에게 최고의 UI 경험을 제공하자.
-
useLayoutEffect
- 리페인트 전에 실행되고, 동기적이다. useEffect()를 실행하게 되면 화면이 깜빡일 수 있다. 처음부터 완전한 화면을 보여줄 수 있다는 것이 장점, 훅 안에서 오랜 시간 스레드를 점유하는 로직이 있다면 단점이 될 수 있다.
-
리액트 헬멧
- 컴포넌트 레벨에서 메타데이터를 설정할 수 있는 라이브러리
-
useInsertionEffect
- 브라우저가 DOM 변경사항을 커밋하기 전에 동기적으로 실행됨.
- 주로
<head>내부에<style>을 삽입하기 위함이며 스타일 태그 삽입 외에는 DOM 조작이나 레이아웃 정보 읽기 시도는 권장되지 않음. - CSS in JS 라이브러리에서 주로 사용됩니다.
-
React 19에서는 메타 데이터와 스타일시트를 어떤 컴포넌트에서든 불러올 수 있다. 다만
<link> 태그의 rel="stylesheet"에서precedence프롭스를 사용하여 우선순위를 정해주어야 한다. 또한 외부 stylesheet를 불러오는 경우<Suspense>경계를 사용할 수 있다. 적용하지 않으면 FOUC 이슈를 경험할 수 있으므로 유의하자. -
useImperativeHandle()은 제어 역전에 쓰이는 hook이다.
-
다들 아시다시피 리액트의 핵심 설계 원칙은 부모에서 자식으로 데이터가 흐르는 데이터 단방향 흐름이다. useImperativeHandle를 통해 자식의 내부 구현은 숨기고, 부모가 ref를 통해 명령형 핸들을 직접 정의할 수 있다.
-
책에 다양한 예시가 나오는데, 그냥 쉽게 이해하면 부모에서 자식에게 구체적으로 명령하는 함수를 구현한다고 이해하면 될 것 같다. 즉, Input 포커스 등의 구체적 액션은 자식 컴포넌트에서 구현하는데, 그걸 부모에서 구체적으로 명령한다는 느낌으로 리액트에서의 제어 역전 개념을 이해했다.
-
동시성에 대한 이야기
-
useTransition은 hook이다. 책의 예시로 설명하면 [isPending, startTransition]을 반환하고, startTransition으로 감싼 함수는 우선순위를 낮춰 상태 업데이트를 하고, 그동안 isPending의 값은 true로 변한다.
-
startTransition은 React 18까지는 비동기 함수를 받으면 제대로 isPending 상태를 반영하지 못했다. React 19부터는 제약이 사라져 비동기 함수를 전달할 수 있다고 한다.
-
useDefferedValue는 상태값을 감싸서 UI 업데이트를 순위를 지연시킨다.
BubbleGenerator.tsx 예시 useEffect(() => { console.log(1); startTransition(() => { console.log(2); setCount(prev => prev + 1); }); console.log(3); }, []);
-
실행결과는 1 2 3
-
UI 업데이트 우선순위를 낮추는 것이지, 실행 자체를 늦추는 것이 아니다.
-
useSyncExternalStore는 외부 상태를 동기화하는 hook이다.
-
외부 상태란 브라우저 API, 서드파티 상태관리 라이브러리 등 외부에서 직접 구독/발행 패턴으로 관리되는 상태이다.
-
이 훅이 없었을 때는 useEffect를 통해 외부 스토어를 구독하고, clean-up 함수에서 구독을 끊는 패턴을 사용했어야 했다.
-
useSyncExternalStore가 해결하는 주요 문제 중 하나는 티어링이다. 티어링은 여러 컴포넌트에서 같은 외부 데이터 소스를 참조할 때 각기 다른 버전의 데이터를 읽어 UI가 일관되지 않는 현상이다.