여러분은 React 앱에서 포커스가 엉뚱한 곳으로 이동하거나, 키보드로 탐색할 때 예상치 못한 곳에서 멈춰본 경험이 있으신가요? 이런 문제는 겉보기에는 사소해 보이지만, 실제로는 사용자 경험의 핵심을 흔드는 치명적인 결함입니다. 오늘은 이런 문제를 근본적으로 해결해주는 React의 숨겨진 API, flushSync
에 대해 자세히 살펴보겠습니다.
React 상태 업데이트의 이중성: 성능과 예측 가능성 사이의 딜레마
React의 배치 업데이트 시스템은 분명 혁신적입니다. setState
를 호출해도 즉시 렌더링하지 않고, 이벤트 핸들러가 끝날 때까지 기다렸다가 한 번에 처리하죠. 이는 성능 측면에서 탁월한 선택이지만, DOM과의 직접적인 상호작용이 필요한 순간에는 예상치 못한 복잡성을 만들어냅니다.
비동기 업데이트가 만드는 함정
가장 간단한 예시를 살펴보겠습니다:
function SimpleComponent() {
const [show, setShow] = useState(false);
const inputRef = useRef(null);
const handleClick = () => {
setShow(true);
inputRef.current?.focus(); // 이 시점에서 input은 아직 DOM에 없습니다!
};
return (
<div>
<button onClick={handleClick}>Show Input</button>
{show && <input ref={inputRef} placeholder="Focus me!" />}
</div>
);
}
이 코드를 실행하면 버튼을 클릭해도 input에 포커스가 가지 않습니다. 왜일까요? setShow(true)
를 호출한 순간에는 아직 React가 리렌더링을 수행하지 않아서, input 엘리먼트가 DOM에 존재하지 않기 때문입니다.
setTimeout과 requestAnimationFrame의 불안정성
많은 개발자들이 이런 문제를 해결하기 위해 다음과 같은 해킹을 시도합니다:
const handleClick = () => {
setShow(true);
setTimeout(() => {
inputRef.current?.focus();
}, 10); // 마법의 숫자에 의존하는 불안정한 해결책
};
하지만 이는 근본적으로 불완전한 접근법입니다. 브라우저의 렌더링 성능, 디바이스의 처리 속도, 다른 스크립트의 실행 상황에 따라 10ms가 충분할 수도, 부족할 수도 있거든요. 특히 저성능 디바이스나 CPU 집약적인 작업이 동시에 실행되는 환경에서는 예측할 수 없는 결과를 낳습니다.
flushSync: 확실성을 제공하는 동기화 도구
react-dom
의 flushSync
는 이런 문제를 근본적으로 해결합니다. 이 API는 React에게 “지금 당장 업데이트를 처리해달라”고 명령하는 강력한 도구입니다:
import { flushSync } from 'react-dom';
function ReliableComponent() {
const [show, setShow] = useState(false);
const inputRef = useRef(null);
const handleClick = () => {
flushSync(() => {
setShow(true);
});
// 이 시점에서 DOM이 확실히 업데이트되어 있습니다
inputRef.current?.focus();
};
return (
<div>
<button onClick={handleClick}>Show Input</button>
{show && <input ref={inputRef} placeholder="Always focused!" />}
</div>
);
}
flushSync의 동작 원리
flushSync
내부에서 발생한 모든 상태 업데이트는 콜백이 완료되는 즉시 DOM에 반영됩니다. 이는 React의 일반적인 비동기 배치 처리를 우회하여 동기적 업데이트를 보장하는 특별한 메커니즘입니다.
EditableText 컴포넌트 구현하기
실제 프로덕션 환경에서 활용할 수 있는 더 복잡한 예시를 살펴보겠습니다. 인라인 편집이 가능한 텍스트 컴포넌트를 구현해보죠:
function EditableText({
initialValue = '',
fieldName,
inputLabel,
buttonLabel
}) {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(initialValue);
const inputRef = useRef(null);
const buttonRef = useRef(null);
const startEditing = () => {
flushSync(() => {
setIsEditing(true);
});
inputRef.current?.select(); // 텍스트 전체 선택
};
const finishEditing = (newValue) => {
flushSync(() => {
setValue(newValue);
setIsEditing(false);
});
buttonRef.current?.focus(); // 원래 버튼으로 포커스 복귀
};
if (isEditing) {
return (
<form onSubmit={(e) => {
e.preventDefault();
finishEditing(inputRef.current?.value ?? '');
}}>
<input
ref={inputRef}
type="text"
aria-label={inputLabel}
name={fieldName}
defaultValue={value}
onKeyDown={(e) => {
if (e.key === 'Escape') {
finishEditing(value); // 변경사항 취소
}
}}
onBlur={(e) => {
finishEditing(e.currentTarget.value);
}}
/>
</form>
);
}
return (
<button
ref={buttonRef}
aria-label={buttonLabel}
onClick={startEditing}
>
{value || 'Click to edit'}
</button>
);
}
이 구현에서 주목할 점은 포커스 흐름의 완벽한 순환입니다:
- 버튼 클릭 → 즉시 input으로 전환 및 포커스
- 편집 완료 → 즉시 버튼으로 전환 및 포커스 복귀
모든 사용자를 위한 설계
포커스 관리가 중요한 이유는 단순히 기능적 완성도 때문만이 아닙니다. 웹 접근성의 핵심 원칙 중 하나가 바로 예측 가능한 키보드 내비게이션입니다.
키보드 사용자의 관점
마우스 없이 키보드만으로 웹을 탐색하는 사용자들을 생각해보세요:
- 시각 장애가 있는 사용자들
- 손목 부상으로 마우스 사용이 어려운 사용자들
- 단순히 키보드 사용을 선호하는 파워 유저들
이들에게 포커스가 예상치 못한 곳으로 이동하거나 사라지는 것은 완전한 길 잃음을 의미합니다. flushSync
를 활용한 정확한 포커스 관리는 이런 사용자들에게 신뢰할 수 있는 경험을 제공합니다.
성능과 안정성 사이의 균형
flushSync
는 강력하지만 신중하게 사용해야 하는 도구입니다. 동기적 업데이트는 성능상 비용이 따르기 때문입니다.
언제 사용해야 할까요?
적절한 사용 사례:
- 상태 변경 직후 DOM 요소에 포커스를 설정해야 할 때
- 서드파티 라이브러리가 즉시 업데이트된 DOM을 필요로 할 때
- 브라우저 API (예:
onbeforeprint
)와의 동기화가 필요할 때
피해야 할 사용 사례:
- 일반적인 상태 업데이트 (React의 기본 배치 처리로 충분)
- 성능이 중요한 빈번한 업데이트
- 사용자 인터랙션과 직접 관련 없는 백그라운드 업데이트
현대적 React 개발에서의 의의
flushSync
는 React의 철학적 진화를 보여주는 좋은 예시입니다. 초기 React는 “선언적”이라는 이름 하에 개발자로부터 DOM 제어권을 완전히 가져갔습니다. 하지만 실제 애플리케이션 개발에서는 때로 imperative한 제어가 필요하다는 것을 인정하고, 이를 위한 탈출구를 제공하는 것이죠.
이는 프론트엔드 개발 생태계가 점점 더 성숙해지고 있음을 의미합니다. 순수한 이론보다는 실용적 해결책을, 완벽한 추상화보다는 필요에 따른 유연성을 추구하는 방향으로 발전하고 있는 것입니다.
디테일이 만드는 차이
포커스 관리는 사용자가 의식적으로 인지하지 못하는 영역입니다. 잘 되어 있을 때는 자연스럽게 느껴지지만, 잘못되었을 때는 즉시 어색함을 만들어냅니다. flushSync
는 이런 미묘한 차이를 만들어내는 도구입니다.
여러분의 React 애플리케이션에서 포커스 관리에 어려움을 겪고 있다면, flushSync
를 고려해보세요. 단순한 API 하나가 사용자 경험의 품질을 한 단계 끌어올릴 수 있을 것입니다.
참고 자료: Kent C. Dodds, “Mastering Focus Management in React with flushSync
“