hooks

99rassh0p3r·2025년 12월 8일

*created at: 2025-12-08
*last updated at: 2025-12-08


참고자료: (번역) 리액트는 이미 변했습니다. 훅 역시 변해야 합니다.

많은 리액트 코드베이스에서 훅(hooks)은 거의 동일한 패턴으로만 사용되고 있다. 흔히 보이는 건 useState, useEffect 위주이고, 반복된 코드 구조.

하지만 원래 훅은 단순히 기존 클래스 컴포넌트의 라이프사이클을 함수형으로 ‘재작성’하기 위한 도구가 아니었다. 훅은 더 표현력 있고, 모듈화된 아키텍처를 구성하기 위한 설계 방식이었다.

문제점으로 저자는 특히 useEffect의 과용을 지적한다. 데이터 가져오기(fetch), 파생 상태 계산, 단순 상태 변화 등 본래 렌더링 결과에 순수하게 종속되는 로직까지 useEffect 안에 넣어두는 경우가 많다. 이로 인해 컴포넌트가 예기치 않은 시점에 재실행되거나, 불필요하게 자주 실행되는 부작용이 생긴다.

제안된 원칙은 다음과 같다:
1. 네트워크 요청, DOM 조작, 구독 등 실제 ‘외부 세계’와 연관된 작업만 useEffect에 넣는다.
2. 순수한 파생 상태나 계산은 렌더링 과정에서 (예: useMemo, useCallback) 처리한다.

만약 useEffect를 사용해야 한다면, 단순히 의존성 배열 관리 피하려고 남용하지 말고, 안정적인 이펙트 콜백 디자인을 위해 useEffectEvent 같은 더 적합한 훅을 고민하라는 제안이 있다.

또한, 리액트 생태계와 자바스크립트 프론트엔드 애플리케이션 설계는 변화 중이라는 점, 특히 React 18 이후 등장한 서버 컴포넌트, 비동기 데이터 로딩, 렌더 중심 데이터 흐름, 구독 기반 상태 관리 등을 고려할 때, 훅 사용 방식도 재고되어야 한다는 관점이 강조된다.

결론적으로, 훅은 단순 문법이 아니라 아키텍처의 일부이다. 파생 상태, 사이드 이펙트, 비동기 흐름, 클라이언트/서버 경계 등 구조적 설계를 고려하며, 단순 반복 대신 의도적인 설계로 훅을 활용해야 한다.

useSyncExternalStore

리액트 18은 동시성 렌더링(concurrent rendering)을 도입했다.
그러나 외부 스토어나 브라우저 API 상태는 리액트의 내부 흐름과 다르게 동작하기 때문에, 렌더링 중간에 값이 바뀌며 화면이 일관성을 잃는 문제가 발생한다.

  • tearing: 렌더링 중 상태가 바뀌면서 화면이 서로 다른 값으로 분열됨
  • 일관되지 않은 값: matchMedia, 스크롤 위치 등 외부 값이 렌더마다 달라짐
  • 고빈도 이벤트 처리 중 성능/정합성 이슈

-

동시성 렌더링(concurrent rendering)은 다음과 같은 특징을 가진다.

  • 컴포넌트를 렌더링하는 작업을 "중단했다가 재개"할 수 있다.
  • 긴 작업을 여러 조작으로 나누어 처리한다.
  • 우선순위가 높은 업데이트를 먼저 처리할 수 있다.

즉, 렌더링이 "즉시 실행 + 즉시 완료"가 아니라, 중단과 우회를 허용하는 비동기적 모델이 된 것 이다.

외부 상태(스토어·브라우저 API)는 동시성 모델을 고려하지 않는다.
(window.matchMedia, window.scrollY, Redux/Zustand 등 외부 스토어, WebSocket 이벤트) 이 값들은 리액트 렌더링과 무관하게, 즉 리액트가 “지금 렌더링 중인지”, “이 렌더가 과거 스냅샷을 사용하는지” 따지지 않고 즉시 변경된다.

따라서 다음 문제가 발생한다.

1. tearing: 한 화면에 서로 다른 버전의 상태가 섞여 보이는 현상
tearing은 “찢어짐”이라는 뜻이다.
어떤 상태 A를 읽는 두 컴포넌트가 있을 때, 하나는 오래된 값(old)을 읽고, 다른 컴포넌트는 새로운 값(new)을 읽는 상황을 말한다.
리액트 동시성의 특성상:

  • 컴포넌트 X는 렌더링 중단 → 재개
  • 그 사이 외부 상태가 변경됨
  • 컴포넌트 Y는 새로운 상태로 렌더

이렇게 되면 화면 구성 요소들이 서로 다른 스냅샷을 참조하고 있어 버전이 일치하지 않는다.
즉, 같은 외부 상태를 읽었는데 화면이 분열되는 현상이 tearing이다.

2. 일관되지 않은 값 문제
동시성 렌더링 중 리액트가 “어제의 스냅샷”을 사용해 컴포넌트를 재평가하고 있지만 외부 값은 이미 바뀐 상태
(화면 폭(matchMedia), 스크롤 위치, 타이머 값, 네이티브 이벤트 기반 값)

컴포넌트 내부에서는 다음과 같은 일이 발생한다.

  • 첫 번째 렌더에서 화면 폭 = 1024
  • 두 번째 렌더에서는 “렌더링 재개 중이므로” 여전히 1024를 사용
  • 그러나 브라우저 API가 반환하는 값은 이미 768, 즉 리얼타임
    하나의 렌더 사이클 안에서 읽히는 값이 불일치할 수 있다.

3. 고빈도 이벤트 처리 중 성능/정합성 이슈

  • 스크롤 이벤트
  • 리사이즈 이벤트
  • 마우스 움직임
  • 실시간 스트림 데이터

이런 이벤트는 매우 빠르게 발생한다.

동시성 렌더링에서는:
1. 리액트가 어떤 컴포넌트를 렌더링 중
2. 하지만 외부 이벤트는 초당 30~100회 발생
3. 렌더 과정이 여러 번 중단될 수 있음
4. 렌더링 중 컴포넌트가 여러 버전의 값(과거 스냅샷과 새 값)을 섞어서 읽을 위험이 있음

즉, 렌더링 중단이 가능한 환경에서는 외부 상태 변화를 그대로 읽으면 정합성(consistency) 이 깨진다.

-

useEffect 기반 구독으로는 위 문제를 막을 수 없다. 그래서 리액트가 공식적으로 도입한 것이 useSyncExternalStore이다.

useSyncExternalStore의 역할

  • 렌더링 전체가 같은 버전의 외부 상태를 읽도록 강제
  • 외부 상태 변경 시 React에게 “정확한 재렌더링 시점”을 알려줌
  • tearing 없이 여러 렌더가 동일한 값을 읽도록 보장
  • SSR 환경에서도 fallback 스냅샷을 제공
  • 서드파티 스토어(Zustand, Redux)도 안정화 가능

즉, 외부 상태에 관해서는 useEffect가 아니라 useSyncExternalStore가 ‘정석적인 경로’가 된다.

useSyncExternalStore는 “외부 상태와 동시성 렌더링 모델을 안전하게 연결하기 위한 공식 프로토콜” 이라고 요약할 수 있다.

function useMediaQuery(query) {
  return useSyncExternalStore(
    (callback) => {
      const mql = window.matchMedia(query);
      mql.addEventListener("change", callback);
      return () => mql.removeEventListener("change", callback);
    },
    () => window.matchMedia(query).matches,
    () => false
  );
}

이 예시는 다음 사실을 명확히 드러낸다.
이벤트 구독은 여전히 필요하지만 그 결과값을 컴포넌트가 안정적으로 읽어야 하므로 useSyncExternalStore가 필수이다.

이 구조는 useEffect에 넣었을 때 발생할 수 있는 tearing을 방지한다.

테스트 가능하고 디버깅하기 쉬운 훅

  • 도메인 로직은 UI와 분리해서 유지합니다.
  • 가능하다면 훅을 직접 테스트합니다.
  • 가독성을 위해 프로바이더 로직은 별도의 훅으로 분리합니다.

훅은 아키텍쳐 패턴입니다

  • 파생 상태는 렌더링 단계에서 유지합니다.
  • 이펙트는 오직 실제 사이드 이펙트에만 사용합니다.
  • 작고 목적이 분명한 훅을 조합해 로직을 구성합니다.
  • 동시성 도구를 활용해 비동기 흐름을 부드럽게 만듭니다.
  • 클라이언트와 서버의 경계를 함께 고려합니다.

알아두면 좋은 API들

  • 렌더링 과정에서 비동기 리소스를 다루기 위한 use() (주로 서버 컴포넌트에서 사용되며, 서버 액션을 통해 제한적으로 클라이언트 컴포넌트에서도 지원됩니다.)
  • 안정적인 이펙트 콜백을 위한 useEffectEvent
  • 워크플로우 형태의 비동기 상태를 위한 useActionState
  • 프레임워크 차원의 캐싱 및 데이터 처리 기본 도구
  • 개선된 동시성 렌더링 도구와 개발자 도구

궁금한 점이나 키워드

  • 동시성 렌더링
  • 정합성
  • useSyncExternalStore의 스냅샷(snapshot)
  • 동시성 렌더링에서 “스냅샷 일관성”이 왜 중요한지
profile

0개의 댓글