반응형 Effect의 라이프사이클 · 퀴즈

7 문항 · Bloom: Understand:3, Analyze:1, Apply:1, Evaluate:2

Q1 Understand mcq_single

컴포넌트 라이프사이클과 Effect 라이프사이클의 관계를 가장 정확히 설명한 것은?

정답: B
컴포넌트는 한 번 마운트되어 살아 있어도, 그 안의 Effect는 reactive 값이 바뀔 때마다 cleanup → 재실행을 반복하며 자기 시계를 따로 돈다. 또한 매 렌더는 그 시점의 props/state를 캡처한 자체 closure를 만들기 때문에 Effect 본문의 식별자는 '그 렌더 시점의 값'을 가리킨다.
오답 해설:
  • A. Effect를 componentDidMount/Will Unmount의 자리로 보는 흔한 오해 — 같은 컴포넌트가 살아있는 동안에도 Effect는 여러 번 connect/disconnect를 겪는다.
  • C. 빈 [] deps도 '한 번만'이 아니라 '재동기화가 필요 없다'는 주장이다. 게다가 컴포넌트가 unmount되면 cleanup이 호출되므로 '종료'의 시점도 다르다.
  • D. 각 렌더는 자기만의 closure를 가진다 — 공유하는 단일 스코프가 아니라 렌더마다 새로운 props/state 스냅샷이 캡처된다.
Q2 Analyze mcq_multi

다음 식별자 중 useEffect deps에 들어가야 하는 'reactive' 값을 모두 고르시오. (정답 2개)

정답: A, C
Reactive = props, state, 그리고 컴포넌트 본문에서 그것들로부터 파생된 변수. roomId는 prop, normalized는 state에서 파생된 값이라 매 렌더에서 다시 계산되며 deps에 들어가야 한다.
오답 해설:
  • B. 모듈 상수는 컴포넌트 밖에서 한 번 정의되어 변하지 않는 non-reactive 값 — deps 생략 OK.
  • D. ref.current의 mutation은 React에 보고되지 않아 Object.is가 변화를 감지할 수 없다. 따라서 non-reactive이며 deps에 넣어도 재실행 신호가 오지 않는다.
  • E. 컴포넌트 밖에서 정의된 함수는 매 렌더마다 새로 만들어지지 않고 React의 데이터 흐름 바깥에 있어 non-reactive.
Q3 Understand mcq_single

ChatRoom의 roomId prop이 'general'에서 'travel'로 바뀌었다. React가 useEffect(..., [roomId])를 처리하는 단계 순서로 가장 정확한 것은?

정답: B
React는 deps 배열을 항목별로 Object.is(prev, next)로 비교하고, 하나라도 false이면 이전 cleanup → 새 본문 실행 순서로 동기화를 다시 한다. 컴포넌트는 그대로 살아있고 새 closure가 새 roomId를 캡처한다.
오답 해설:
  • A. cleanup은 새 본문 실행 '이전'에 호출된다 — 좀비 연결을 막기 위한 순서.
  • C. roomId 변경은 컴포넌트 unmount/remount가 아니라 같은 인스턴스 안에서의 update — Effect만 재동기화 사이클을 돈다.
  • D. React는 ===가 아니라 Object.is로 비교한다(NaN, -0 처리 차이). 또 cleanup은 unmount까지 보류되지 않고 deps 변화 즉시 실행된다.
Q4 Understand true_false

useEffect(() => { ... }, []) 의 빈 [] 는 'Effect를 컴포넌트 lifetime 동안 한 번만 실행한다'는 의도를 표현하는 표준 방법이다.

정답: B
거짓. 빈 [] 는 '이 Effect는 어떤 reactive 값도 읽지 않으니 재동기화가 필요 없다'는 주장이다. Strict Mode dev 더블 마운트, unmount 시 cleanup 등은 여전히 일어나며, 만약 본문이 reactive 값을 몰래 읽고 있다면 그 주장은 거짓말이 되어 stale closure 버그로 이어진다.
오답 해설:
  • A. 흔한 오해 — '한 번만'으로 받아들이면 reactive 값을 읽고도 deps를 비워두는 안티패턴으로 이어진다.
Q5 Evaluate mcq_single

다음 코드에서 ESLint react-hooks/exhaustive-deps가 theme를 deps에 추가하라고 경고한다. 그러나 채팅 연결은 theme이 바뀌어도 재연결되면 안 된다. 가장 적절한 해결 전략은? ```js useEffect(() => { const c = createConnection(serverUrl, roomId); c.on('message', (m) => showNotification(m, theme)); c.connect(); return () => c.disconnect(); }, [roomId]); // theme missing ```

정답: C
옵션 3 — '최신 값은 필요하지만 재동기화는 싫다'는 정확히 useEffectEvent의 용도. 비-반응형 부분(알림 표시)을 추출해 reactive 의존성에서 분리한다.
오답 해설:
  • A. linter suppress는 절대 금지 — closure가 굳어 stale theme로 알림이 가는 디버깅 지옥을 만든다.
  • B. theme를 deps에 넣으면 theme이 바뀔 때마다 채팅이 재연결된다 — 의도한 동기화 경계를 무너뜨리는 잘못된 처방.
  • D. ref로 우회하는 패턴은 React가 권장하지 않는 안티패턴 — 같은 의도를 정식 도구로 표현하는 useEffectEvent가 정답.
Q6 Evaluate mcq_single

다음 Effect는 채팅 연결과 방문 분석 로깅을 한 번에 처리한다. 어떻게 리팩터링하는 것이 'Effect 분리' 원칙에 가장 부합하는가? ```js useEffect(() => { logVisit(roomId); const c = createConnection(serverUrl, roomId); c.connect(); return () => c.disconnect(); }, [roomId]); ```

정답: B
두 동작은 의미상 무관한 동기화 — '방 방문 로깅'과 '실시간 연결'은 다른 시스템이다. 각자 별도 Effect로 분리하면 추후 logVisit이 userId를 같이 로깅해야 할 때 채팅 연결을 건드리지 않고 첫 Effect의 deps만 늘리면 된다.
오답 해설:
  • A. 현재 deps가 우연히 같다고 같이 둘 이유가 되지 않는다 — 한쪽 정책이 바뀌면 양쪽이 함께 재실행되어 동기화 경계가 무너진다.
  • C. 렌더 중 호출은 순수성을 깨고 Strict Mode 더블 렌더 시 두 번 로깅된다 — 부수효과는 Effect 안에 있어야 한다.
  • D. useEffectEvent는 '최신 값은 필요하나 재동기화는 싫을 때'의 도구 — 여기선 logVisit이 roomId 변화에 정확히 반응해야 하므로 잘못된 도구 선택.
Q7 Apply short_answer

다음 Effect는 한 Effect 안에 (1) 윈도우 resize 리스너 등록과 (2) roomId에 따른 채팅 연결을 함께 처리하고 있다. 'Effect 분리' 원칙에 따라 두 Effect로 쪼갠 코드를 작성하고, 각각의 deps가 왜 그렇게 정해지는지 한 줄씩 근거를 적으시오. ```js useEffect(() => { const onResize = () => setSize(window.innerWidth); window.addEventListener('resize', onResize); const c = createConnection(serverUrl, roomId); c.connect(); return () => { window.removeEventListener('resize', onResize); c.disconnect(); }; }, [roomId]); ```

정답: 분리된 두 Effect 작성 + 각 deps 근거
모범 답안: ```js useEffect(() => { const onResize = () => setSize(window.innerWidth); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); // reactive 값을 안 읽으므로 재동기화 불필요 useEffect(() => { const c = createConnection(serverUrl, roomId); c.connect(); return () => c.disconnect(); }, [roomId]); // roomId(reactive)가 바뀔 때마다 재연결 ``` resize 리스너는 어떤 reactive 값도 읽지 않으므로 [], 채팅 연결은 reactive prop인 roomId를 읽으므로 [roomId].채점 기준:
  • [1점] 두 개의 별도 useEffect로 분리되어 있다 (한 Effect = 한 동기화).
  • [1점] resize Effect의 deps가 [] 이고 cleanup으로 removeEventListener를 호출한다.
  • [1점] 채팅 Effect의 deps가 [roomId] 이고 cleanup으로 disconnect를 호출한다.
  • [1점] 각 deps의 근거가 reactive/non-reactive 분류로 설명되어 있다(특히 [] 가 '재동기화 불필요'의 의미라는 점).
  • 감점: 두 Effect를 하나로 둔 경우, eslint-disable로 처리한 경우, ref.current/모듈 상수를 deps에 넣은 경우.