06

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를 반환한다.
  • 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가 일관되지 않는 현상이다.