← 목차
state 구조 설계 — 5가지 원칙으로 동기화 버그 차단 · 퀴즈
8 문항 · Bloom: Understand:2, Apply:2, Analyze:2, Evaluate:2 ·
v1.0.0
Q1
Understand
mcq_single
다음 중 Section 2에서 다룬 'state 구조 설계 5원칙'에 해당하지 않는 것은?
A.
A. 관련 state는 함께 갱신된다면 객체로 묶어라
B.
B. 모순되는 boolean 조합을 만들지 말고 status enum으로 단일화하라
C.
C. 다른 state나 props로 계산 가능한 값은 state로 두지 말라
D.
D. 가능한 모든 state는 useReducer로 옮겨 단일 store에 보관하라
정답: 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.
A. `useState({ x: 0, y: 0 })` 단일 객체 state로 묶고 `setPosition({ x: e.clientX, y: e.clientY })`로 한 번에 갱신한다
B.
B. 그대로 두 개의 state로 유지한다 — 변수가 분리되어 있을수록 항상 더 안전하다
C.
C. `useReducer`로 옮겨 ACTION_MOVE를 dispatch 한다
D.
D. `useRef({ x, y })`로 옮겨 렌더를 줄인다
정답: 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.
A. redundant state — `fullName`은 state에서 제거하고 렌더 시 `const fullName = firstName + ' ' + lastName`으로 derive 한다
B.
B. props mirroring — `initialFullName` prop으로 바꾼다
C.
C. duplication — `fullName`을 id로 바꿔 보관한다
D.
D. deep nesting — fullName을 객체로 묶어 평탄화한다
정답: 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.
A. 사례 ②만 — `defaultName`을 `initialName`으로 이름 바꾸고 useState 시드로 둔다. 사례 ①은 `const color = messageColor`로 직접 사용한다
B.
B. 사례 ①만 — initial 접두사로 부모 변경을 의도적으로 무시해야 한다
C.
C. 둘 다 initial 접두사가 정답이다 — props를 초기값으로 받는 모든 경우에 적용된다
D.
D. 둘 다 직접 사용으로 바꿔야 한다 — useState로 prop을 받는 것은 항상 잘못이다
정답: 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.
A. 진단: items와 selectedItem이 같은 정보를 두 곳에 보관해 SSOT가 깨졌다. 처방: `selectedId`만 state에 두고 `selectedItem = items.find(i => i.id === selectedId)`로 derive 한다
B.
B. 진단: handlePriceChange가 selectedItem을 함께 갱신하지 않은 단순 누락 버그다. 처방: setItems 다음 줄에 setSelectedItem도 호출한다
C.
C. 진단: items가 객체 배열이라 불변 갱신이 잘못됐다. 처방: items를 평탄화된 byId map으로 바꾼다
D.
D. 진단: useState가 객체 참조를 비교하지 못한다. 처방: useReducer로 옮긴다
정답: 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.
A. 깊이 D의 노드 삭제 시 부모 체인 전체를 복사할 필요 없이 부모의 childIds와 byId 항목 두 곳만 수정하면 된다
B.
B. 다른 부모로 이동(move)할 때 두 부모의 childIds만 손대면 된다
C.
C. 화면에 계층 구조를 그릴 때 트리 재구성 비용이 사라진다
D.
D. state 자체에서 부모-자식 관계가 자동으로 추론되어 childIds 배열이 필요 없게 된다
E.
E. React가 평탄화된 객체를 자동으로 얕은 비교에서 깊은 비교로 전환한다
정답: 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 하나로 단일화하는 것이 적절하다.
참 (True)
거짓 (False)
정답: 참
참. 모순 가능한 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점 보너스] 깊은 트리에서는 평탄화의 동기가 동기화 버그가 아니라 갱신 비용이라는 점을 분리해 짚는다
제출하고 채점하기