Ref로 DOM 조작하기 — focus, scroll, 그리고 통제된 노출 · 퀴즈

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

Q1 Apply mcq_single

다음 코드에서 첫 클릭 시 input에 포커스가 들어가게 하려면 어디에 `inputRef.current.focus()`를 두어야 하는가? ```jsx function SearchBox() { const inputRef = useRef(null); // ① 여기 (return 위쪽 본문) return ( <> <input ref={inputRef} /> <button onClick={/* ② 여기 */}>Focus</button> </> ); } ```

정답: B
ref.current는 commit 이후에야 채워진다. 이벤트 핸들러는 commit 이후 실행되므로 onClick 안에서 안전하게 `.focus()`를 부를 수 있다.
오답 해설:
  • A. 흔한 오해: 렌더 본문은 commit 이전이므로 ref.current는 아직 null이다. 첫 렌더에서 즉시 크래시한다.
  • C. ref 속성은 React가 노드 부착에 쓰는 자리이지 메서드 호출 트리거가 아니다.
  • D. useRef 호출 시점에는 DOM이 존재하지 않는다. ref.current는 null이다.
Q2 Understand mcq_single

render → commit → effect 타이밍 모델에서 `ref.current`의 상태로 가장 정확한 설명은?

정답: B
React는 commit phase에서 DOM을 생성/갱신하며 ref.current를 채운다. 업데이트 시에도 노드 교체 직전 null로 리셋했다가 다시 설정한다 — 그래서 핸들러/Effect에서만 안전하다.
오답 해설:
  • A. render phase에 ref가 '이전 노드를 가리킨다'는 보장은 없다. 업데이트 직전 React가 null로 초기화한다.
  • C. useRef 호출은 단지 `{ current: null }` 객체를 만들 뿐이며, JSX에 부착되어 commit이 일어나야 노드가 들어온다.
  • D. ref.current mutation은 재렌더를 트리거하지 않는다 — 이것이 useState와의 핵심 차이다.
Q3 Analyze mcq_single

투두 리스트에 새 항목을 추가한 직후 자동으로 맨 아래로 스크롤하려고 한다. 다음 핸들러가 한 박자 어긋나 **이전 마지막 항목**으로 스크롤되는 원인은? ```jsx function handleAdd() { setTodos([...todos, newTodo]); listRef.current.lastChild.scrollIntoView(); } ```

정답: C
기본적으로 setState는 batched이며 이벤트 핸들러가 끝난 뒤 한 번에 commit된다. 따라서 setTodos 다음 줄에서는 아직 옛 DOM이 보이고 lastChild는 이전 마지막 항목이다. flushSync로 동기 commit을 강제해 해결한다.
오답 해설:
  • A. smooth는 시각 효과일 뿐 어느 노드로 스크롤되는지에는 영향이 없다.
  • B. 핸들러는 commit 이후에 실행되므로 listRef.current는 이미 채워져 있다. 문제는 새 자식이 아직 추가되지 않았다는 점이다.
  • D. scrollIntoView는 동기 호출이며 React가 큐잉하지 않는다.
Q4 Apply mcq_single

위 Q3 문제를 `flushSync`로 고친 코드로 가장 적절한 것은?

정답: B
flushSync는 react-dom에서 import하며, 콜백 안에서 호출된 setter를 동기 commit하도록 강제한다. flushSync 다음 줄에서는 새 DOM이 보장되므로 lastChild가 새 항목을 가리킨다.
오답 해설:
  • A. flushSync는 'react'가 아닌 'react-dom'에서 import한다. 또한 인자는 함수여야 하는데 setTodos 호출 결과(undefined)를 넘겼다.
  • C. flushSync로 감싸야 할 것은 setter다. scrollIntoView를 감싸는 것은 효과가 없다.
  • D. setTimeout(..., 0)은 우연히 동작할 수 있지만 batching 보장과 무관한 race를 만든다 — flushSync가 의도가 명시적인 정공법이다.
Q5 Analyze mcq_multi

다음 중 **위험한** ref 사용 패턴을 모두 고르시오. (정답 2개)

정답: A, C
React state와 실제 DOM의 일관성은 React가 JSX로 그린 노드를 우리가 임의로 추가/제거할 때 깨진다. A는 state-DOM 불일치로 충돌, C는 React가 모르는 형제 노드가 끼어들어 reconcile 시 충돌·크래시 발생.
오답 해설:
  • B. 안전 영역의 정석. 'JSX에서 늘 비어 있는 컨테이너' 내부는 React가 손대지 않으므로 third-party 라이브러리에 위임 가능하다.
  • D. play/pause는 노드를 추가/제거하지 않는 명령형 메서드 호출일 뿐이다 — 안전.
  • E. focus는 읽기/명령형 호출일 뿐 노드 구조를 바꾸지 않는다 — 안전.
Q6 Apply mcq_single

React 19 환경에서 자체 디자인 시스템의 `<MyInput />`이 부모에게 ref를 받게 하는 가장 자연스러운 코드는?

정답: B
React 19에서는 ref가 일반 prop처럼 동작해 함수 시그니처에서 그대로 분해할 수 있다. forwardRef 래핑이 사라지면서 디자인 시스템 코드가 단순해진다.
오답 해설:
  • A. React 18까지의 정공법. React 19에서 새로 도입할 이유는 없으며 마이그레이션 맥락이 아니라면 더 깔끔한 ref-as-prop을 쓴다.
  • C. React 18 이전에는 ref가 props에 자동으로 들어오지 않는다. React 19부터만 ref가 prop이며, 그때도 분해 패턴이 일반적이다 — 게다가 보기 코드는 컴파일러 경고를 부르는 혼란스러운 형태다.
  • D. 자체 ref를 만들면 부모가 외부에서 input에 접근할 수 없다 — 부모의 ref는 무시되며 요구사항을 충족하지 못한다.
Q7 Evaluate mcq_single

비디오 플레이어 래퍼 `<VideoPlayer ref={...} />`를 설계할 때 부모에게 `play()`와 `pause()`만 노출하고 `currentTime`, `volume` 같은 내부 API는 막고 싶다. 가장 적절한 설계는?

정답: B
useImperativeHandle로 부모에게 노출되는 객체를 직접 설계하면, 자식의 내부 구현(예: video를 다른 요소로 교체)을 바꿔도 부모 코드가 깨지지 않는다. '명령형 API의 의도적 좁히기' = 캡슐화 가치를 지키는 선택이다.
오답 해설:
  • A. ref 위임만으로는 부모가 .currentTime, .volume 등 video DOM 전체에 접근하게 되어 캡슐화가 깨진다 — 정확히 막아야 할 상황이다.
  • C. 전체 노드를 넘기는 것은 위임과 동일하며 문서화는 강제력이 없다. 코드 레벨에서 막아야 한다.
  • D. 재생/정지 같은 인터랙션 결과를 부모가 직접 트리거해야 하는 명령형 시나리오는 useImperativeHandle의 정당한 사용처다. '전혀 쓰지 않는다'는 과잉 일반화.