함수형 컴포넌트는 클로저를 기반으로 동작합니다. 이로 인해 발생하는 'stale closure(오래된 클로저)' 문제가 무엇이며, setInterval이나 이벤트 구독 안에서 state를 참조할 때 왜 의도치 않은 값이 보이는지, 그리고 이를 해결하는 방법을 설명해주세요.
힌트 · 각 렌더가 자신만의 props/state 스냅샷을 캡처한다는 점과 의존성 배열·ref를 통한 해결 관점에서 접근하세요.
모범답안
함수형 컴포넌트는 렌더링될 때마다 새로운 함수 스코프를 생성하고, 이 스코프는 해당 렌더링 시점의 props와 state 값을 '캡처'합니다. 이것이 바로 클로저의 동작 방식입니다.
'Stale closure' 문제는 이 캡처된 오래된 스코프가 계속 유지되어 발생하는 현상입니다. 예를 들어, setInterval이나 이벤트 리스너 안에서 state 값을 참조할 때, 컴포넌트가 재렌더링되어 state가 업데이트되었더라도, setInterval이나 이벤트 리스너는 처음 생성될 때의 오래된 state 값을 계속 참조하게 됩니다. 그래서 의도치 않게 이전 값이나 예상치 못한 값이 보이는 것이죠.
이 문제를 해결하는 방법은 크게 두 가지입니다.
첫째, useEffect의 의존성 배열을 활용하는 것입니다. useEffect의 두 번째 인자인 의존성 배열에 state나 props를 명시하면, 해당 값이 변경될 때마다 useEffect 내부의 함수가 다시 실행되어 최신 값을 참조하게 됩니다. 하지만 setInterval처럼 주기적으로 실행되어야 하는 경우, 의존성 배열에 모든 state를 넣으면 너무 자주 클린업되고 다시 설정되어 성능 문제가 발생할 수 있습니다.
둘째, useRef를 사용하는 것입니다. useRef는 컴포넌트의 생명주기 동안 값이 유지되면서도, 값이 변경되어도 컴포넌트를 재렌더링하지 않습니다. 따라서 setInterval이나 이벤트 리스너 안에서 최신 state 값을 참조하고 싶을 때, 해당 state를 useRef에 저장해두고 useRef의 .current 값을 참조하면 stale closure 문제를 피할 수 있습니다. useEffect 안에서 useRef를 업데이트하고, setInterval 등에서는 useRef를 참조하는 방식으로 구현하면 됩니다.
읽었다면, 이제 직접 답해볼 차례예요
패스잇 앱에서 이 질문에 말로 답하면 AI가 꼬리질문까지 이어가며 1:1 코칭합니다.
함께 보는 React 면접 질문
- React의 useImperativeHandle Hook의 용도와 사용법에 대해 설명해주세요.
- React에서 setState 호출이 항상 비동기적으로 동작한다고 말할 수 있을까요? React 18 이전과 이후의 자동 배치(automatic batching) 동작 차이를 setTimeout이나 Promise 콜백 내부에서의 setState를 예로 들어 설명해주세요.
- JSX는 결국 React.createElement(또는 jsx 런타임) 호출로 변환됩니다. 이 변환 결과인 element 객체가 어떤 구조를 가지며, 이것이 실제 DOM이 아니라 '설명(description)'에 불과하다는 점이 렌더링 파이프라인에서 어떤 의미를 갖는지 설명해주세요.
- 단방향 데이터 흐름을 따르는 React에서, 부모-자식 간 상태를 끌어올리는(lifting state up) 패턴이 깊은 컴포넌트 트리에서 'prop drilling' 문제를 일으킵니다. 이 문제를 Context, composition(children 합성), 상태 관리 라이브러리 중 무엇으로 풀지 판단하는 기준을 설명해주세요.