React의 useState, useRef, 일반 변수의 생명주기 이해하기
React의 상태 관리와 생명주기 이해하기
안녕하세요! 오늘은 React에서 상태를 관리하는 세 가지 방법 - useState, useRef, 일반 변수의 차이점과 생명주기 내에서의 동작을 이해해보려고 합니다. 특히 useEffect와 클린업 함수가 이와 어떻게 상호작용하는지에 초점을 맞춰 보겠습니다.
문제: 이 코드의 출력 결과는?
아래 코드를 보고 어떤 결과가 출력될지 예상해 보세요:
"use client";
import { useEffect, useRef, useState } from "react";
export default function Test1() {
const [data, setData] = useState(0);
const aRef = useRef(data);
let a = 1;
useEffect(() => {
console.log("let", a);
console.log("useState", data);
console.log("useRef", aRef.current);
setData((prev) => prev + 1);
aRef.current = data;
a = data;
return () => {
console.log(
"cleanup a",
a,
"clean up data",
data,
"clean up aRef ",
aRef.current
);
};
}, [data]);
return <div>Test</div>;
}
왜 이 문제를 만들었나요?
이 문제는 React의 다음 중요 개념들을 이해하는 데 도움이 됩니다:
-
리렌더링 시 값 보존 방식의 차이
- useState로 관리되는 값
- useRef로 관리되는 값
- 함수 컴포넌트 내의 일반 변수
-
클로저(Closure)가 React 생명주기에 미치는 영향
- 이펙트와 클린업 함수가 캡처하는 값들
-
useEffect의 실행 순서와 클린업 함수의 호출 시점
코드의 실행 결과와 설명
이 코드를 실행하면 다음과 같은 출력 결과가 나옵니다:
// 첫 번째 렌더링
let 1
useState 0
useRef 0
// 두 번째 렌더링 (data가 1이 됨)
cleanup a 0 clean up data 0 clean up aRef 0
let 1
useState 1
useRef 0
// 세 번째 렌더링 (data가 2가 됨)
cleanup a 1 clean up data 1 clean up aRef 1
let 1
useState 2
useRef 1
// ... 계속 ...
왜 이런 결과가 나올까요?
첫 번째 렌더링
-
컴포넌트가 마운트되고 초기값들이 설정됩니다:
- data = 0 (useState 초기값)
- aRef.current = 0 (useRef 초기값으로 data 사용)
- a = 1 (일반 변수 초기값)
-
useEffect가 실행됩니다:
- console.log('let', a) → let 1 출력
- console.log('useState', data) → useState 0 출력
- console.log('useRef', aRef.current) → useRef 0 출력
- setData((prev) => prev + 1) → data를 1로 업데이트 (비동기적으로 실행)
- aRef.current = data → aRef.current를 0으로 설정 (이 시점에서 data는 아직 0)
- a = data → a를 0으로 설정
두 번째 렌더링
-
data가 1로 변경되어 컴포넌트가 리렌더링됩니다.
-
함수 컴포넌트가 다시 실행되면서:
- data = 1 (이전 렌더링에서 업데이트된 값)
- aRef.current = 0 (이전 렌더링에서 설정한 값 유지)
- a = 1 (함수가 재실행되므로 초기값으로 다시 설정)
-
이전 useEffect의 클린업 함수가 실행됩니다:
- 'cleanup a', a → 첫 번째 렌더링의 useEffect 내에서 수정된 a 값인 0 출력
- 'clean up data', data → 첫 번째 렌더링 시점의 data 값인 0 출력
- 'clean up aRef ', aRef.current → 현재 시점의 aRef.current 값인 0 출력 (아직 첫 번째 렌더링에서 설정한 값)
-
새 useEffect가 실행됩니다:
- console.log('let', a) → let 1 출력 (함수가 재실행되어 초기화됨)
- console.log('useState', data) → useState 1 출력
- console.log('useRef', aRef.current) → useRef 0 출력
- setData((prev) => prev + 1) → data를 2로 업데이트
- aRef.current = data → aRef.current를 1로 설정
- a = data → a를 1로 설정
핵심 개념 정리
1. 함수 컴포넌트의 실행과 값 초기화
함수 컴포넌트는 렌더링이 필요할 때마다 전체가 다시 실행됩니다. 이 때:
- 일반 변수(let a = 1)는 항상 초기값으로 재설정됩니다.
- useState의 값은 React에 의해 보존되어 새 렌더링에 전달됩니다.
- useRef의 .current 값은 렌더링 간에 유지됩니다.
2. 클로저와 useEffect
useEffect와 그 안의 클린업 함수는 클로저의 원리로 동작합니다:
- 각 렌더링마다 useEffect 내부에서 사용하는 변수들(a, data, aRef.current)은 해당 렌더링 시점의 값으로 캡처됩니다.
- 클린업 함수도 자신이 정의된 렌더링 사이클의 값들을 캡처하여 기억합니다.
- 클린업 함수는 비록 다음 렌더링 시점에 실행되지만, 그 함수 내부에서 참조하는 모든 변수들은 함수가 정의된 렌더링의 useEffect 내에서 수정된 최종 값을 사용합니다.
- 중요한 것은 모든 값(a, data, aRef.current)이 동일한 값으로 출력된다는 점입니다. 이는 모두 같은 시점(useEffect 내에서 a = data, aRef.current = data로 수정된 후)의 값을 캡처하기 때문입니다.
이는 React의 "클로저 캡처" 특성의 전형적인 예시입니다. 클린업 함수는 정의될 때의 환경(렉시컬 환경)을 기억하고, 그 환경에서의 최종 값을 사용합니다.
위 예제에서 확인할 수 있듯이:
- 첫 번째 렌더링에서 정의된 클린업 함수는 useEffect 실행 후 수정된 값(a=0, data=0, aRef.current=0)을 사용합니다.
- 두 번째 렌더링에서 정의된 클린업 함수는 그 시점의 수정된 값(a=1, data=1, aRef.current=1)을 사용합니다.
- 세 번째 렌더링에서는 모두 2의 값을 사용합니다.
이처럼 클린업 함수는 항상 자신이 정의된 렌더링의 useEffect에서 설정된 최종 값들을 사용합니다. 따라서 항상 세 값이 동일하게 출력되는 것입니다.
3. useRef vs useState
- useRef는 값을 저장하지만 변경해도 리렌더링을 트리거하지 않습니다.
- useState는 값을 저장하고 변경하면 컴포넌트의 리렌더링을 트리거합니다.
- 둘 다 렌더링 간에 값을 유지하지만, 목적과 사용 방식이 다릅니다.
4. 클린업 함수의 실행 시점과 값 캡처
클린업 함수는 다음 경우에 실행됩니다:
- 컴포넌트가 언마운트될 때
- 의존성 배열의 값이 변경되어 이펙트가 다시 실행되기 전에
중요한 점: 클린업 함수는 다음 렌더링 시점에 실행되지만, 그 함수 내부에서 참조하는 변수들은 함수가 정의된 렌더링 시점의 값을 사용합니다. 이것이 React의 클로저 기반 동작 방식입니다.
위 예제에서 확인할 수 있듯이:
- 첫 번째 렌더링에서 정의된 클린업 함수는 useEffect 실행 후 수정된 값(a=0, data=0, aRef.current=0)을 사용합니다.
- 두 번째 렌더링에서 정의된 클린업 함수는 그 시점의 수정된 값(a=1, data=1, aRef.current=1)을 사용합니다.
이런 동작을 이해하는 것은 사이드 이펙트를 관리할 때 매우 중요합니다.
결론
이 예제를 통해 React의 상태 관리 방식과 생명주기에 대해 더 깊이 이해할 수 있습니다. 특히 클로저가 React의 동작에 미치는 영향을 이해하는 것은 예측 가능한 컴포넌트를 작성하는 데 큰 도움이 됩니다.
클로저, 리렌더링, 그리고 각 상태 관리 방식의 차이점을 제대로 이해하면 더 효율적이고 버그가 적은 React 코드를 작성할 수 있을 것입니다.