FEInterview Prep

Tistory

상태 기반 렌더링 vs 시그널 기반 렌더링

리렌더링이 '상태가 만들어진 곳' 에서 시작되느냐, '상태가 읽히는 곳' 에서만 일어나느냐의 차이는 단순한 성능이 아니라 반응성에 대한 사고방식 자체의 전환이다.

2025-11-07·7분 읽기
ReactJavaScript성능
원문 보기 ↗

핵심 요약

리액트 훅에서 useState 가 업데이트되면 그 컴포넌트와 모든 자식 이 재실행된다. 자식이 그 값을 쓰는지 여부는 무관하다. 따라서 메모이제이션은 "불필요한 자식 렌더를 막는 보호막" 으로 존재한다.

반면 시그널은 의존성을 스스로 추적하는 반응형 원시 단위 다. signal.value 를 실제로 읽은 컴포넌트만 구독에 등록되고, 시그널이 바뀌면 그 구독자만 다시 그려진다 — 컴포넌트 트리 구조와 무관하다.

// React hook — 자식 전체 리렌더
const Parent = () => {
  const [count, setCount] = useState(0);
  return (<>
    <ChildA />        {/* count 안 써도 리렌더 */}
    <ChildC count={count} />
  </>);
};

// Signal — 값 읽는 곳만 리렌더
const Parent = () => {
  const count = useSignal(0);
  return (<>
    <ChildA />        {/* 리렌더 X */}
    <ChildC count={count} />
  </>);
};

컨텍스트와 결합하면 차이가 더 극명해진다. 훅 + Context 는 모든 구독자를 깨우지만, 시그널 + Context 는 .value 를 읽은 구독자만 깨운다. 결과적으로 React.memo / useMemo / useCallback 같은 보호막이 거의 사라진다.

리액트 훅은 "상태가 사는 곳에서 페인트가 흘러내린다" — 위에서 아래로 다시 그려진다. 시그널은 "값을 읽는 곳에 줄이 매여 있다" — 그 줄을 잡아당기는 컴포넌트만 다시 그려진다. 면접에서 이 두 멘탈 모델을 한 줄씩으로 분리해 말할 수 있느냐가 깊이의 분기점이다.

리액트 최적화 면접의 단골 질문이 바뀌고 있다. 예전엔 React.memo / useMemo / useCallback 의 차이를 외우는 것으로 충분했지만, 이제는 "왜 그런 메모이제이션이 필요한가" 의 근원을 묻는다.

  • 훅 모델은 불필요한 리렌더를 막는다 가 디폴트
  • 시그널 모델은 필요한 곳에서만 리렌더가 일어난다 가 디폴트

이 차이를 이해하면 Solid.js, Preact Signals, Vue 3, Angular Signals 가 왜 비슷한 방향으로 수렴하는지 한 줄로 설명할 수 있다. 사고방식이 바뀐 것이지 단순히 빠른 라이브러리가 나온 게 아니다.

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

기준점의 차이 — '컴포넌트' 가 아니라 '값을 읽은 자리'

두 모델의 가장 큰 차이는 무엇이 리렌더 단위인가다.

모델리렌더 단위트리거
컴포넌트 (그리고 자식 트리)상태가 정의된 곳에서 setState
시그널값을 읽은 JSX 표현식signal.value 변경

시그널 모델에서는 컴포넌트의 위치 가 의미가 없다. 데이터에 매여 있을 뿐이다. 이 한 문장이 프롬 드릴링·메모이제이션·Context 분할이 거의 사라지는 이유다.

granular reactivityreactive primitive구독(subscription)fine-grained
자주 하는 오해

'시그널이 더 빠르다' 로 외우는 것. 빠른 게 아니라 리렌더 단위가 작아진 것이고, 단위가 작으면 자연히 적게 일하는 결과가 따라온다.

Context 결합에서 격차가 폭발

리액트의 Context API 는 "값이 바뀌면 모든 구독자 리렌더" 가 디폴트다. name 만 쓰는 컴포넌트도 count 가 바뀌면 다시 그려진다.

// React Context — 무관한 값 변경에도 리렌더
const { name } = useContext(CountContext);
return <div>{name}</div>; // count 바뀌면 리렌더

// Signal in Context — 읽은 시그널만 구독
const { name } = useContext(CountContext);
return <div>{name.value}</div>; // count 변경 무관

이 때문에 리액트 진영에서는 "Context 를 잘게 쪼개라" 가 모범 사례다. 시그널 + Context 는 그 쪼개기 자체가 불필요해진다 — Context 는 의존성 주입 만 담당하고 반응성은 시그널이 알아서 한다.

Context APIselector patternDIsubscriber set
자주 하는 오해

Context 분할을 우선시하는 습관. 시그널 환경에서는 그 비용 대부분이 사라지므로 "먼저 분할하지 말고 시그널을 사용하라" 가 새 디폴트다.

메모이제이션 헬퍼가 사라지는 이유

훅 환경에서 React.memo / useMemo / useCallback자식 렌더 폭주를 막기 위한 방어 코드다. 시그널 환경에서는 자식이 자기 의존성에 따라서만 그려지므로 이 방어 코드가 무의미해진다.

결과:

  • 번들 크기 감소 (수동 메모이제이션 제거)
  • 코드 가독성 향상
  • 디버깅이 쉬워짐 — "누가 리렌더되었나""어디서 시그널을 읽었나" 로 환원

다만 언제 시그널이 적합한가 는 별개다. 작은 앱·낮은 업데이트 빈도면 훅 모델로 충분하고, 큰 트리·고빈도 업데이트(애니메이션, 실시간) 일수록 격차가 커진다.

useMemoReact.memomanual memoizationfan-out
자주 하는 오해

시그널이 항상 더 좋다고 단정하는 것. 트레이드오프 — 학습 곡선, 라이브러리 호환성, 디버깅 도구 성숙도 — 가 분명히 있다.

읽는 순서

  1. 1이론

    원문(Jovi De Croock, State-based vs Signal-based rendering)과 Solid.js 의 createSignal 문서를 함께 읽고, "리렌더 단위" 를 한 줄로 정의해 노트에 적으세요.

  2. 2구현

    동일한 카운터 트리를 두 번 만들어 비교: (a) useState + Context, (b) @preact/signals-react 또는 Solid createSignal. React DevTools Profiler 로 자식 컴포넌트 리렌더 횟수 를 측정합니다.

  3. 3실무

    현재 프로젝트에서 React.memo 또는 useMemo 가 가장 많이 붙어 있는 모듈을 찾고, 그 자리가 시그널이라면 무엇이 사라질지 매핑하세요. 실제로 작은 일부만 시그널로 옮겨 PoC 합니다.

  4. 4설명

    팀에 5분 발표 — '왜 우리 앱의 X 화면은 시그널 후보인가, 왜 Y 화면은 아닌가'. 트리 깊이/업데이트 빈도/Context 의존성 세 축으로 근거를 제시합니다.

면접 연결 질문

medium시그널 기반 렌더링이 `React.memo` 를 쓸모없게 만드는 이유를 한 문장으로 설명해보세요.
힌트

[감점 답변] '시그널이 빠르니까'. [좋은 답변] 리렌더 단위가 컴포넌트가 아니라 '값을 읽은 자리' 로 바뀌기 때문이다. React.memo부모 리렌더가 자식으로 전파되는 것을 차단 하는 헬퍼인데, 시그널 모델에선 부모 리렌더가 자식에게 전파되지 않으므로 차단할 대상 자체가 없다.

medium리액트 Context + useState 패턴과 시그널 + Context 패턴의 차이가 면접에서 자주 나오는 이유를 설명해보세요.
힌트

[감점 답변] '둘 다 전역 상태'. [좋은 답변] 구독 단위가 다르다. Context + useState 는 Provider 의 value 가 바뀌면 모든 useContext 구독자가 리렌더 된다. 시그널 + Context 는 Context 가 시그널 객체를 운반 하고, 컴포넌트는 .value 를 읽은 시점에 시그널 자체를 구독한다. 따라서 같은 Provider 안에서도 어떤 값을 읽느냐 에 따라 리렌더 주체가 달라진다.

hard'시그널이 항상 우월한가' — 트레이드오프를 두 가지 이상 말해보세요.
힌트

[감점 답변] '아니요'. [좋은 답변] 다음 트레이드오프를 든다.

  • 생태계: 리액트 메인 라이브러리/툴킷 대부분이 훅 기반 — 시그널 도입 시 호환성 작업
  • 학습 곡선: .value / useSignal / For / Show 같은 새 멘탈 모델 — 팀 온보딩 비용
  • DevTools 성숙도: React DevTools 만큼 성숙한 도구가 부족
  • 단순한 앱: 리렌더 비용이 어차피 작은 앱에서는 이득이 미미

큰 트리 + 고빈도 업데이트 + 깊은 Context 같은 조건에서 효용이 극대화된다.

자기 점검

Preact 의 `<For each={items}>` 같은 제어 흐름 컴포넌트가 왜 "세밀한 반응성" 을 가능하게 하는지 설명해보세요.
scopeJSX 자식computed리렌더 범위
자주 하는 오해

리스트 항목을 key 로 식별하는 것과 같다고 보는 오해. key 는 차이를 식별 하고, For반응성의 범위를 한정 한다 — 다른 층위의 메커니즘이다.

컴포넌트 트리가 깊고 prop drilling 이 심한 앱에서 시그널 도입 시 리팩터링이 *작은* 이유를 설명해보세요.
DIContext값 전달구독
자주 하는 오해

'전부 시그널로 바꿔야 한다' 는 인식. 실제로는 상태 정의 지점만 시그널화 해도 자식들이 자연스럽게 구독자가 되어 리렌더 범위가 좁아진다.

본인 프로젝트에서 시그널 도입을 고려할 때 *먼저* 측정할 메트릭 두 가지는?
리렌더 횟수Long TaskINP프로파일링
자주 하는 오해

벤치마크 점수만 보고 결정하는 것. 실제 사용자 경험은 리렌더 횟수와 INP/Long Task 의 함수다.