state 구조 설계 — 5가지 원칙으로 동기화 버그 차단 · 퀴즈

8 문항 · Bloom: Understand:2, Apply:2, Analyze:2, Evaluate:2 · v1.0.0

Q1 Understand mcq_single

다음 중 Section 2에서 다룬 'state 구조 설계 5원칙'에 해당하지 않는 것은?

정답: D
5원칙은 '관련 state 묶기 / 모순 회피 / redundant 제거 / duplication 회피 / deep nesting 평탄화'다. useReducer는 Section S5 주제이며, 구조 설계 원칙과는 결이 다르다.
오답 해설:
  • A. 원칙①(관련 state 묶기)을 정확히 기술한다.
  • B. 원칙②(모순 회피)를 정확히 기술한다.
  • C. 원칙③(redundant 제거)을 정확히 기술한다.
Q2 Apply mcq_single

마우스 위치를 추적하는 컴포넌트가 다음과 같이 두 개의 state를 가지고 있다. ```jsx const [x, setX] = useState(0); const [y, setY] = useState(0); // onPointerMove 안에서 항상 둘 다 setX, setY를 함께 호출한다 ``` 원칙①(관련 state 묶기)을 적용한다면 어떻게 재구조화하는 것이 가장 적절한가?

정답: A
묶기 결정 기준은 '항상 함께 갱신되는가'다. x와 y는 한 이벤트에서 동시에 갱신되므로 객체로 묶어 단일 setState로 만든다. 이러면 한쪽만 갱신되어 모순이 생기는 일이 구조적으로 차단된다.
오답 해설:
  • B. 분리 보관은 '항상 함께 갱신' 시 두 setState가 흩어져 일관성을 잃을 위험을 키운다. '분리가 항상 안전하다'는 명제는 거짓이다.
  • C. useReducer는 액션이 다양하거나 전이가 복잡할 때 가치가 있다. 좌표 동시 갱신만을 위해 도입하면 과잉이다.
  • D. 위치 변화로 UI가 갱신되어야 하는 컴포넌트라면 useRef는 화면을 다시 그리지 않아 부적절하다.
Q3 Apply mcq_single

다음 코드는 어떤 안티패턴을 위반하고 있으며, `fullName`은 어떻게 다뤄야 하는가? ```jsx function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirst(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } } ```

정답: A
`fullName`은 `firstName`과 `lastName`으로 매 렌더 동일하게 계산할 수 있는 derived value다. state로 두면 갱신 경로 하나가 누락되는 순간 동기화가 깨진다. 처방은 state 제거 후 렌더 시 변수로 계산하기.
오답 해설:
  • B. props가 등장하지 않는 코드이므로 mirroring 안티패턴이 아니다.
  • C. 선택된 객체 보관 문제(원칙④)는 list와 selectedItem 같은 시나리오에 해당한다. 단순 결합값과는 무관하다.
  • D. 트리 형태 데이터가 아니므로 deep nesting과 무관하다.
Q4 Analyze mcq_single

다음 두 컴포넌트 모두 `useState(props.X)` 형태로 prop을 초기값에 넣고 있다. 이 중 'initial 접두사로 의도를 드러내는' 해법(해법 B)이 적절한 사례는 어느 것인가? ```jsx // 사례 ① — 부모가 사용자의 테마 변경에 따라 동적으로 messageColor를 바꾼다 function Message({ messageColor }) { const [color, setColor] = useState(messageColor); return <div style={{ color }}>...</div>; } // 사례 ② — 부모는 첫 렌더 때만 defaultName을 넘겨주고, 이후엔 사용자가 입력란에서 자유롭게 편집한다 function EditableForm({ defaultName }) { const [name, setName] = useState(defaultName); return <input value={name} onChange={e => setName(e.target.value)} />; } ```

정답: A
선택 기준은 '부모의 갱신을 자식이 따라가야 하나, 아니면 첫 값만 시드로 쓰나?'다. 사례 ①은 부모 변경을 즉시 반영해야 하므로 직접 사용. 사례 ②는 사용자가 편집 중일 때 부모 덮어쓰기가 오히려 버그이므로 initial 접두사로 '시드일 뿐'임을 API에 노출한다.
오답 해설:
  • B. 사례 ①에서 부모의 색상 변경이 자식에게 반영되지 않으면 그 자체가 버그다. initial 접두사는 의미가 다르다.
  • C. 직접 사용이 적절한 경우(사례 ①)도 있으므로 일률 적용은 틀렸다.
  • D. 편집 폼 시드 같은 합법적 use case가 존재하므로 '항상 잘못'은 거짓이다.
Q5 Evaluate mcq_single

장바구니 컴포넌트가 다음과 같이 selectedItem 객체 자체를 state에 보관하고 있다. ```jsx const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState(items[0]); function handlePriceChange(id, newPrice) { setItems(items.map(i => i.id === id ? { ...i, price: newPrice } : i)); } ``` 사용자가 'Pretzels'(현재 selectedItem)의 가격을 $2 → $5로 수정했지만 'Selected' 패널은 여전히 $2를 보여준다. 이 시나리오에서 어느 진단과 처방이 가장 적절한가?

정답: A
원인은 duplication이다. items와 selectedItem이 진실의 원천을 둘로 쪼개고 있어 한쪽 갱신이 다른 쪽에 자동 반영되지 않는다. 처방은 id만 보관하고 list에서 derive — SSOT 실천. 동기화 호출을 양쪽에 추가하는 방법(B)도 동작은 하지만 새 갱신 경로가 추가되는 순간 다시 깨진다. 구조로 차단해야 한다.
오답 해설:
  • B. 동작은 하지만 똑같은 일을 두 곳에서 해야 하는 빚을 떠안는다. 새 갱신 경로 한 군데만 누락돼도 다시 stale이 된다 — 구조적 해결이 아니다.
  • C. items의 불변 갱신은 정상(map + spread)이다. 평탄화는 깊이 있는 트리 데이터에 적용하는 다른 원칙이다.
  • D. useReducer는 동기화 버그를 자동으로 막아주지 않는다. 같은 duplication 버그가 dispatch 안에서 똑같이 재현된다.
Q6 Analyze mcq_multi

다음은 깊이 5단계 `childPlaces` 트리를 byId map + childIds 배열로 평탄화한 상태다. ```js { 0: { id:0, title:'Earth', childIds:[1,42,46] }, 1: { id:1, title:'Americas', childIds:[2,10] }, 2: { id:2, title:'USA', childIds:[5] }, 5: { id:5, title:'Pasadena', childIds:[] } } ``` 이 평탄화 구조의 이점으로 옳은 것을 모두 고르시오. (정답 2개)

정답: A, B
평탄화의 핵심 이득은 갱신 범위 축소다. 삭제는 부모 childIds + byId[N] 삭제 두 군데, 이동은 두 부모 childIds 두 군데로 끝난다. 트리 재구성은 렌더 시점 비용으로 그대로 남으며(C 오답), 부모-자식 관계는 childIds 배열로 명시적으로 표현해야 한다(D 오답).
오답 해설:
  • C. 오히려 화면에 계층 구조를 그릴 때마다 byId+childIds로 트리를 재구성해야 한다. 다만 재귀 컴포넌트가 자연스럽게 처리한다.
  • D. 평탄화 구조에서 부모-자식 관계는 명시적으로 childIds 배열에 보관해야 하며 자동 추론되지 않는다.
  • E. React의 비교 정책은 state 모양과 무관하다. 얕은 비교 의미론은 그대로 유지된다.
Q7 Understand true_false

참/거짓: '두 개의 boolean 변수 isLoading, isError가 동시에 true가 되면 의미상 불가능한 상태'라면 원칙②(모순 회피)를 적용해 `status: 'idle' | 'loading' | 'error' | 'success'` 같은 enum 하나로 단일화하는 것이 적절하다.

정답: 참
참. 모순 가능한 boolean 조합은 impossible state를 허용하므로 status enum으로 단일화해 구조적으로 차단한다. S2.C1에서 다룬 원칙②의 정확한 적용이다.
오답 해설:
  • 거짓. boolean 분리 보관은 isLoading=true && isError=true 같은 불가능 상태를 코드 차원에서 막지 못한다.
Q8 Evaluate short_answer

팀원이 '평탄화된 byId 구조도 좋지만, 트리 모양 그대로 보관한 뒤 list 갱신 핸들러에서 selectedItem도 함께 setState 해주면 동기화 버그를 막을 수 있다'고 주장한다. 두세 문장으로 이 주장에 대해 평가하시오 — 어떤 부분은 동작하고, 어떤 부분이 결국 깨지는가?

정답: rubric
모범 답안 핵심: (1) '함께 setState'가 단기적으로는 동작하지만 똑같은 일을 두 곳에서 해야 하므로 새 갱신 경로가 추가되는 순간 다시 깨진다. (2) 동기화는 코드 컨벤션이 아니라 구조로 차단해야 한다 — id만 보관하고 derive 하면 누락 가능한 호출 자체가 사라진다. (3) 깊은 트리의 경우 부모 체인 전체 복사 비용도 따로 남아 평탄화의 동기는 또 다르다.채점 기준:
  • [1점] '동작은 한다'는 단기적 사실을 인정한다
  • [1점] 새 갱신 경로 누락 시 다시 깨진다는 근본 한계를 지적한다
  • [1점] '구조로 차단 vs 컨벤션으로 보완'의 대비 또는 SSOT 개념을 명시한다
  • [+1점 보너스] 깊은 트리에서는 평탄화의 동기가 동기화 버그가 아니라 갱신 비용이라는 점을 분리해 짚는다