useState
useState가 클로저 원리를 어떻게 이용해서 state값의 불변성을 유지하는지 너무 잘 보여주는 코드 예시 하나를 발견했다.
function useState(initialValue) {
let state = initialValue; // 상태 값을 저장하기 위한 변수
function setState(newValue) {
state = newValue; // 상태 값을 갱신하기 위한 함수
}
function getState(){
return state;
}
return [getState(), setState]; // 상태 값과 갱신 함수를 반환
}
여기서 `setState`, `getState`는 자신을 내포한 함수의 지역변수인 `state`를 기억해서 useState 함수가 return된 이후에도 `state` 값을 계속해서 참조할 수 있다.
useEffect
useEffect는 사이드 이펙트를 정리할 때도 쓰이는데, 여기에도 클로저의 원리가 담겨있다.
사이드 이펙트(side effect): 외부의 상태를 변경하는 것.
- 리액트에서는 컴포넌트에서 리액트 외부의 상태(네트워크 리퀘스트, 메모리 할당 등)를 바꾸는 것을 의미한다.
일상생활에서는 주로 안 좋은 효과를 말할 때 사이드 이펙트(부작용)라고 부르지만, 프로그래밍에선 말 그대로 이외 부수적인 작용을 하는 걸 말한다. - useEffect는 DOM 노드를 변경하거나,네트워크 리퀘스트를 보내거나, 브라우저에 데이터를 저장하는 사이드 이펙트를 실행하고 싶을 때 사용된다. useEffect 더 알아보기🔗
useEffect에서는 return 함수(클린업 함수)가 클로저인데, useEffect의 콜백에서 일어난 사이드 이펙트를 여기서 정리한다.
중요한 것은 현재 일어난 사이드 이펙트가 아니라, '이전에' 일어난 사이드 이펙트를 정리한다는 점이다.
이게 가능하려면 이전에 일어난 사건을 '기억'하고 있어야 한다. 여기서 클로저 원리가 사용된다.
예시1) setInterval
useEffect(() => {
const id = setInterval(() => {
console.log('타이머 작동 중');
}, 1000);
return () => {
clearInterval(id); // 클린업 함수: 이전 타이머를 정리
};
}, []);
이 코드의 setInterval은 사이드 이펙트를 발생시킨다. Web API이기 때문에, 브라우저가 이를 실행함으로써 브라우저의 상태가 바뀌기 때문이다. 그리고 useEffect의 return 함수는 1초에 한 번씩 타이머를 작동시키는 setInterval을 '기억'하고 있다가, 리렌더링 될 때마다 clearInterval을 통해 '이전의' 사이드 이펙트를 정리한다.
여기서 return 함수는 렌더링 사이나 컴포넌트 언마운트 시점(=다음 useEffect 실행 시점)에만 실행되며, 그때마다 이전의 setInterval을 기억해서 해제한다.
이렇게 이전 값을 기억해서 정리할 수 있는 이유는 '클린업 함수(return 함수)가 클로저'이기 때문이다.
코드 심층 분석 🔍
return () => {
clearInterval(id);
};
- 이 함수는 'useEffect 콜백 함수 내부(= const id가 존재했던 스코프)'에서 선언됨.
- 자바스크립트는 이 함수를 클로저로 만들고, 이 함수는 id가 담긴 렉시컬 환경을 함께 기억하게 된다.
👉 리턴된 이후에도 자신이 기억하고 있는 렉시컬 환경의 'id'를 계속해서 사용할 수 있음.
👉 클린업 함수의 클로저 원리를 통해 리액트가 안전하게 리소스를 정리할 수 있음.
🔄 useEffect의 실행 흐름 (중요 포인트)
useEffect(() => {
const id = setInterval(() => {
console.log('interval 실행');
}, 1000);
return () => {
clearInterval(id);
console.log('interval 해제');
};
}, [count]); // count가 바뀔 때마다 useEffect의 콜백 실행
1. 최초 렌더링 시
- useEffect 내부 코드 실행 → setInterval 등록 (id 저장)
- id는 클로저에 의해 클린업 함수가 기억함
- 클린업 함수는 아직 호출되지 않음
2. count가 바뀌어 리렌더링될 때
- 먼저 기존의 클린업 함수가 실행됨
- 여기서 이전 렌더링 당시의 id를 이용해 clearInterval(id) 실행 - 그 후 새로운 setInterval이 설정됨
- 그리고 새로운 클린업 함수가 다시 등록됨
3. 언마운트 시
- 마지막 클린업 함수가 실행되어, 타이머 정리
⚠️ 여기서 주의할 점은, 1초에 한 번씩 사이드 이펙트(setInterval)가 일어날 때마다 정리 함수가 실행되는 게 아니라,
위에서도 언급했지만 '다음 useEffect 실행 시점'에만 실행된다는 것이다.
useEffect의 정리 함수는 setInterval의 콜백이 아니라 React 렌더링 사이클에 의해 실행되기 때문이다.
예시2) file input
useEffect(() => {
if (!value) return;
const nextPreview = URL.createObjectURL(value);
setPreview(nextPreview);
return () => {
setPreview();
URL.revokeObjectURL(nextPreview);
};
}, [value]);
이 코드는 file input으로 이미지 파일을 첨부했을 때, 브라우저로부터 임의의 이미지 주소를 nextPreview에 할당받아 preview라는 state에 저장하고, 해당 state를 img 태그의 src로 첨부하여 이미지 미리보기를 생성한다.
여기서는 `URL.createObjectURL` 구문에 의해 사이드 이펙트가 발생한다. 임시 이미지 주소가 메모리(=리액트 외부)에 할당되기 때문이다.
의존성 배열에는 value 상태가 추가되어 있는데, 이는 해당 컴포넌트의 부모 컴포넌트에서 내려온 상태로, 첨부 파일에 대한 정보가 담겨있다. (file input은 보안상 비제어 컴포넌트여야 하므로 직접 value 상태를 관리하지 않는데, 여기서는 해당 내용을 생략하겠다.)
그러므로 첨부 파일을 바꿀 때마다 value값이 갱신되고, useEffect의 콜백도 재실행되는데 여기서 콜백이 재실행되기 전에 먼저 return 함수를 실행함으로써 이전에 등록된 사이드 이펙트를 정리한다.
👉 여기서도 마찬가지로 클로저의 원리가 작용하여,
return 함수가 useEffect 콜백 내부 스코프에서 할당된 임시 이미지 주소를 '기억'하고 있었으므로 해당 주소를 정리(할당 해제)할 수 있게 된다.
요약
useEffect의 return 함수(=정리 함수=클린업 함수)는 클로저이며,
이전의 사이드 이펙트를 기억하고 있다가 다음 useEffect의 콜백이 실행되기 전에 해당 이펙트를 정리한다.
useRef
'JS' 카테고리의 다른 글
| this를 명시적으로 설정하는 2가지 함수, .call()과 .bind()의 차이 + .apply() (0) | 2025.11.14 |
|---|---|
| slice vs splice (0) | 2025.05.15 |
| 함수 파라미터를 중괄호로 감싸서 받을 때와 그냥 받을 때의 차이 (0) | 2025.05.10 |
| 자주 사용되는 JavaScript Web API들 (0) | 2025.05.10 |
| Promise.all() 사용하기 (0) | 2025.04.25 |