Effect 의존성 줄이기 — 6가지 전략 · 퀴즈

8 문항 · Bloom: Understand:1, Apply:2, Analyze:4, Evaluate:1

Q1 Understand mcq_single

다음 중 'deps는 코드의 거울이다'라는 원칙과 그에 따른 워크플로를 가장 정확하게 설명한 것은?

정답: B
deps는 Effect 코드가 사용한 reactive 식별자의 거울이다. 줄이고 싶으면 거울이 아니라 원본(코드)을 바꾼 뒤 linter를 다시 따른다는 4단계 워크플로가 핵심이다.
오답 해설:
  • A. suppress는 frozen closure 버그(예: 카운터가 1에서 멈춤)를 미래로 미루는 안티패턴이다.
  • C. ref로 최신 값을 읽는 것은 reactive 동기화 의도를 우회하는 방법이며 이 섹션의 표준 전략이 아니다.
  • D. useMemo/useCallback은 부모 측 임시방편일 뿐, 자식 입장의 원시값 분해/Effect 내부 생성 같은 코드 변경이 우선이다.
Q2 Apply mcq_single

다음 setInterval 코드를 매 tick마다 재생성되지 않게 고치려고 한다. 가장 적절한 한 줄 수정은? useEffect(() => { const id = setInterval(() => setCount(count + 1), 1000); return () => clearInterval(id); }, [count]);

정답: B
updater 함수 setCount(c => c + 1)는 React가 최신 state를 인자로 넣어주므로 Effect가 count를 직접 읽을 필요가 없어 deps에서 제거된다. interval은 mount 시 한 번만 등록된다.
오답 해설:
  • A. suppress는 첫 렌더 closure를 frozen으로 캡처해 카운터가 1에서 멈추는 버그를 만든다.
  • C. ref는 reactive하지 않아 동작은 할 수 있으나 이 시나리오의 정석 전략은 updater이며, ref는 렌더에 반영되지 않는 비-렌더 데이터용이다.
  • D. useMemo는 값을 캐시하는 용도이지 의존성 문제를 풀지 않으며, deps에 count가 남으면 매 tick 재생성 문제도 그대로다.
Q3 Apply mcq_single

부모가 <ChatRoom options={{ roomId, serverUrl }} />로 객체 prop을 매 렌더 새로 만들어 내려주어, 자식 Effect의 deps=[options]가 매 렌더마다 달라져 무한 재연결이 발생한다. 자식 ChatRoom의 가장 정직한 리팩터링은?

정답: B
객체 prop이 매 렌더 새 참조라는 게 근본 원인이다. 원시값(string)으로 평탄화하면 Object.is 비교에서 안정되고, 객체는 Effect 안에서 생성하므로 deps에 들어가지 않는다(전략 6 + 전략 2 조합).
오답 해설:
  • A. 자식에서 useMemo로 감싸도 입력이 매번 새 객체이므로 의미가 없다. 부모 측에서 안정화하지 않는 한 효과가 없다.
  • C. suppress는 의존성을 거짓말하게 만들어 stale closure 버그로 이어진다.
  • D. roomId/serverUrl은 props로 흘러오는 reactive 값이므로 module scope로 옮길 수 없다. 진짜 상수만 외부화 가능하다.
Q4 Analyze mcq_single

ChatRoom Effect 안에서 새 메시지 도착 시 알림 소리를 내는데, isMuted를 deps에 넣어두니 사용자가 음소거 토글을 누를 때마다 채팅이 끊겼다 다시 붙는다. isMuted는 알림 여부 판단에만 쓰이고 변경되어도 재연결은 필요 없다. 가장 적절한 전략은?

정답: C
'읽고 싶지만 변경에 반응하고 싶지 않다'는 비-반응형 시나리오의 전형이다. useEffectEvent로 감싸면 항상 최신 isMuted를 보면서도 deps에 잡히지 않아 재연결이 사라진다.
오답 해설:
  • A. isMuted는 state(reactive)이므로 컴포넌트 밖으로 옮길 수 없다. 외부화는 진짜 상수에만 적용된다.
  • B. updater는 'state를 읽기만 해서 setter에 넣는' 패턴용이지, 비-반응형 분기의 해법이 아니다.
  • D. Effect 분리는 무관한 두 동기화가 한 Effect에 묶였을 때 쓰는 전략이며, 여기는 단일 동기화 안의 비-반응형 값 문제다.
Q5 Analyze mcq_multi

다음 코드의 문제점과 적용해야 할 전략을 모두 고르시오 (정답 3개). function ChatRoom({ options, isMuted }) { useEffect(() => { const c = createConnection(options); c.on('message', m => { setMessages([...messages, m]); if (!isMuted) playSound(); }); c.connect(); return () => c.disconnect(); }, [options, messages, isMuted]); }

정답: A, B, C
세 증상이 동시 존재한다: (1) options 객체 prop은 부모 렌더마다 새 참조 → 전략 6으로 평탄화. (2) messages를 읽기 위해 deps에 넣었으니 → 전략 3 updater. (3) isMuted는 비-반응형 분기 → 전략 4 useEffectEvent. 이 케이스는 단일 전략으로 풀리지 않으며 조합이 답이다.
오답 해설:
  • D. suppress는 frozen closure 버그를 만들어 안 된다 — 늘 코드를 바꿔야 한다.
  • E. createConnection은 이미 외부 함수이고, options/roomId 등은 reactive 값이므로 외부화 대상이 아니다.
Q6 Analyze mcq_single

한 Effect 안에 country 목록 fetch와 city 목록 fetch가 함께 들어 있고 deps=[country, city]로 묶여 있다. country가 바뀌면 city fetch까지 헛 호출되는 비효율이 있다. 어느 전략이 가장 적절하며 그 이유는?

정답: B
무관한 두 동기화가 한 Effect에 묶일 때의 정석 처방은 분리다. 분리 후 각 Effect의 deps가 [country], [city]로 깨끗해지고 cleanup race도 단순해진다.
오답 해설:
  • A. updater는 setter에 state를 인자로 받는 패턴용이며, 두 동기화 묶임 문제와는 무관하다.
  • C. useEffectEvent는 비-반응형 분기 추출용이지, 두 reactive 의존성을 분리하는 도구가 아니다.
  • D. 원시값 분해는 객체 prop의 새 참조 문제 해결이지 '한 Effect에 두 동기화' 문제와 직교한다.
Q7 Analyze true_false

참/거짓: 'props로 받은 isActive 같은 boolean을 useEffectEvent로 감싸 deps에서 빼면 의존성이 더 깔끔해지므로, 가능하면 모든 reactive 값에 대해 useEffectEvent를 우선 적용하는 것이 좋다.'

정답: 거짓
거짓이다. useEffectEvent는 '읽긴 하지만 변경에 반응하면 안 되는 비-반응형 로직' 추출에만 써야 한다. 정말로 변화에 동기화가 필요한 reactive 값까지 감싸면 동기화가 일어나야 할 시점에 일어나지 않는 silent bug가 생긴다. 또 useEffectEvent는 실험적 API이고 Effect 안에서만 호출 가능하다는 제약도 있어 '디폴트 전략'으로 쓰면 안 된다.
Q8 Evaluate short_answer

다음 ChatRoom 컴포넌트에 대해, 어느 전략 조합을 적용할지 결정하고 가독성·캡슐화·마이그레이션 비용 3축으로 트레이드오프를 정당화하시오. (호출부는 5곳, 팀은 안정 API만 선호하지만 useEffectEvent 사용은 허용) function Form({ initialValues, onSubmit }) { useEffect(() => { reset(initialValues); register(onSubmit); }, [initialValues, onSubmit]); } // 부모: <Form initialValues={{ name: '' }} onSubmit={(d) => save(d)} />

모범 답안 윤곽: (1) initialValues 객체 prop은 매 렌더 새 참조 → 호출부가 5곳뿐이라면 전략 6(원시값 분해, 예: name/email prop으로 평탄화)이 가장 정직 — 마이그레이션 비용은 5곳 수정으로 감당 가능하고 캡슐화도 명확. 호출부가 더 많거나 필드가 가변적이면 전략 2(Effect 안에서 객체 생성)로 자식에 흡수. (2) onSubmit 인라인 함수 prop도 매 렌더 새 참조 → 전략 4(useEffectEvent로 wrap)이 호출부에 영향 없이 캡슐화 ↑. 다만 useEffectEvent의 'Effect 안에서만 호출' 제약을 지킬 것. 결론: 전략 6 + 전략 4 조합. 트레이드오프: 가독성 ↑(deps가 [name] 같은 원시값으로 의도가 명확), 캡슐화 ↑(자식이 reset/register 책임 흡수), 마이그레이션 비용 보통(호출부 5곳 prop 변경). 채점 rubric (4점 만점): - 1점: initialValues가 매 렌더 새 참조라는 원인 식별 - 1점: onSubmit이 매 렌더 새 참조라는 원인 식별 - 1점: 적절한 전략(6 또는 2 + 4) 명시 및 그 선택 근거 제시 - 1점: 가독성/캡슐화/마이그레이션 비용 3축 중 최소 2축에 대한 비교 언급