리액트에서 useState를 사용할 때, 우리는 종종 다음과 같은 코드를 작성하곤 합니다.
const Example = () => {
const initVal = [1,2,3,4,5].reduce((acc, cur) => acc + cur, 0);
const [state, setState] = useState(initVal);
return <div>{state}</div>;
};
문제는 이 코드가 리렌더링마다 불필요한 계산을 반복한다는 것입니다.
이러한 낭비를 막기 위해 React가 제공하는 기능이 바로 Lazy Initialization(지연 초기화) 입니다.
이번 글에서는
를 실전 코드와 함께 명확하게 정리해봅니다.
다음 코드를 보면, reduce() 계산은 컴포넌트가 렌더링될 때마다 다시 실행됩니다.
const Example = () => {
const initVal = [1,2,3,4,5].reduce((acc, cur) => acc + cur, 0);
const [state, setState] = useState(initVal);
return <div>{state}</div>;
};
이 계산이 무거운 연산이라면(예: JSON 파싱, 복잡한 계산, localStorage 읽기, API 유사 연산 등)
컴포넌트가 리렌더링될 때마다 성능을 갉아먹게 됩니다.
사실 이 값은 초기 렌더링 시 한 번만 계산하면 충분한 값입니다.
그 후에는 React가 state를 보관해 주기 때문에 다시 계산할 필요가 없죠.
React는 이러한 불필요한 재계산을 막기 위해
초기값을 값(value) 이 아닌 함수(function) 형태로 전달할 수 있게 제공합니다.
const Example = () => {
const [state, setState] = useState(
() => [1,2,3,4,5].reduce((acc, cur) => acc + cur, 0)
);
return <div>{state}</div>;
};
이제 reduce()는 다음 상황에서만 실행됩니다.
React의 실제 동작을 간단히 요약해 보면 다음과 같습니다.
initialState // type -> () => S | S
if (typeof initialState === 'function') {
const initialStateInitializer = initialState;
initialState = initialStateInitializer();
}
즉,
이것이 Lazy Initialization이 “지연”이라는 이름을 가진 이유입니다.
예를 들어 다음과 같이 작성하는 경우입니다:
const initVal = [1,2,3,4,5].reduce((acc, cur) => acc + cur, 0);
const Example = () => {
const [state, setState] = useState(initVal);
return <div>{state}</div>;
};
겉보기에는 한 번만 계산되는 것처럼 보일 수 있습니다.
하지만 이 방식은 Lazy Init과 완전히 동일하지 않습니다.
다음 표로 비교해보겠습니다.
| 항목 | Lazy Initialization | 컴포넌트 밖 계산 |
|---|---|---|
| 계산 시점 | 컴포넌트 mount 시 | 앱 로드 시 즉시 실행 |
| 불필요한 계산 지연 | 가능 | 불가능 |
| props 기반 초기화 가능 | 가능 | 불가능 |
| 컴포넌트가 실제로 렌더링될 때만 계산 | O | X |
| 컴포넌트가 여러 번 mount될 때 | mount마다 새 계산 | 전역 상수 재사용 |
컴포넌트 밖 초기화는 앱이 시작될 때 이미 계산을 실행해버린다.
Lazy Init은 해당 컴포넌트가 실제로 렌더링되기 전까지 계산을 미룸.
예:
const Example = ({ numbers }) => {
const [sum] = useState(() => numbers.reduce(...));
};
컴포넌트 밖에서는 numbers를 사용할 수 없다.
컴포넌트가 여러 번 mount되면 Lazy Init은 mount마다 독립적으로 계산을 실행하지만,
전역 초기값은 항상 동일한 값을 재사용한다.
Lazy Initialization을 이해하는 가장 좋은 방법 중 하나는,
“이걸 안 썼을 때 어떤 문제가 생기는지”를 보는 것입니다.
const Example = ({ data }) => {
// ❌ 데이터가 클수록 렌더링 비용이 점점 커진다
const parsed = JSON.parse(JSON.stringify(data)); // deep copy 같은 무거운 작업이라고 가정
const [state, setState] = useState(parsed);
// state 업데이트 → 리렌더 → 또 JSON.parse 실행…
// 실제로는 초기값으로 한 번만 필요했는데 계속 반복됨
return <div>{state.length}</div>;
};
setState가 호출될 때마다 이 컴포넌트는 리렌더링되고,JSON.parse 같은 무거운 연산을 매번 다시 수행합니다.이럴 때 Lazy Init을 사용하면:
const Example = ({ data }) => {
const [state, setState] = useState(() => {
return JSON.parse(JSON.stringify(data));
});
return <div>{state.length}</div>;
};
이제 JSON.parse는 초기 mount 시에 단 한 번만 호출되고,
그 이후에는 state 값만 업데이트/재사용됩니다.
const Example = () => {
// ❌ 매 렌더링마다 localStorage를 읽음
const saved = localStorage.getItem('theme') ?? 'light';
const [theme, setTheme] = useState(saved);
return <div>{theme}</div>;
};
브라우저 스토리지 접근은 생각보다 비용이 크고, I/O 성격을 띄기 때문에
매 렌더마다 호출하는 건 좋지 않은 패턴입니다.
const Example = () => {
const [theme, setTheme] = useState(() => {
return localStorage.getItem('theme') ?? 'light';
});
return <div>{theme}</div>;
};
이렇게 Lazy Init을 쓰면,
🔗 React 공식 문서에서도 useState의 초기값으로 함수를 넘길 수 있다는 점을 명시하고 있습니다.
If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render.
useState에 함수를 넘기면, React는 그 함수를 초기 렌더링 시에만 호출하고
그 반환값을 초기 state로 사용합니다. (의역)이를 사용하면 비용이 큰 초기화 로직의 실행을 지연시킬 수 있습니다.
정리하면 공식 문서도 다음을 권장하고 있다고 볼 수 있습니다.
useState(() => heavyWork()) 형태로 Lazy Initialization을 사용하는 것이 좋다.즉, 이 패턴은 단순 트릭이나 내부 구현 디테일이 아니라,
React가 공식적으로 지원하고 있는 성능 최적화 패턴입니다.
Lazy Initialization을 이해할 때
“React는 왜 리렌더링을 할까?”를 같이 생각해보면 더 직관적으로 와닿습니다.
기본적으로 React는 다음과 같은 이유로 컴포넌트를 리렌더링합니다.
이때 중요한 포인트는:
리렌더링이 될 때마다, 함수형 컴포넌트는 다시 호출된다는 점입니다.
즉,
const Example = () => {
const initVal = heavyWork();
const [state, setState] = useState(initVal);
...
}
이 컴포넌트는 리렌더링될 때마다 다시 호출되므로
heavyWork()도 매 리렌더마다 다시 호출됩니다.
반면 Lazy Initialization은, 리렌더링이 되어도
heavyWork()가 다시 호출되지 않도록 막아줍니다.
const Example = () => {
const [state, setState] = useState(() => heavyWork());
...
}
여기서 heavyWork()는
React가 리렌더링을 자주 하더라도, 불필요한 초기 계산이 반복되지 않도록 보호해 주는 것입니다.
useState의 Lazy Initialization은 단순히 “초기값을 함수로 넘기는 문법적 선택”이 아닙니다.
React의 렌더링 방식과 매우 깊게 연결된 구조적인 최적화 기법입니다.
컴포넌트가 리렌더될 때 함수형 컴포넌트가 다시 호출된다는 특성 때문에 초기값 계산을 그대로 두면 불필요한 연산이 반복됩니다. 이 문제를 해결하기 위해 React는 초기값 계산을 컴포넌트가 mount되는 시점으로 명확히 고정해 줄 수 있는 Lazy Initialization을 제공합니다.
정리하면 Lazy Initialization은 다음과 같은 역할을 수행하는 기능입니다:
비용이 큰 초기값 연산을 컴포넌트 mount 시 딱 한 번만 실행한다.
→ 리렌더링이 발생해도 초기값 계산은 다시 실행되지 않는다.
불필요한 계산을 줄여 렌더링 성능을 크게 개선할 수 있다.
컴포넌트 밖에서 초기값을 계산하는 방식과 비슷해 보이지만,
초기화 시점·props 의존성·컴포넌트별 독립성 등에서 중요한 차이가 존재한다.
→ Lazy Init은 더 유연하고 정확한 초기화 방식을 제공한다.
React 공식 문서에서도 명확하게 권장하는 성능 최적화 기법이다.
즉, Lazy Initialization은 초기 state 계산을 제어함으로써
불필요한 렌더링 비용을 줄이고 컴포넌트의 예측 가능성과 안정성을 높여주는 React의 정교한 기능이다.
복잡한 연산, 스토리지 접근, 동적 초기값 계산 등 초기화 비용이 큰 상황에서 Lazy Initialization을 적극 활용하면 더 효율적이고 성능 친화적인 React 컴포넌트를 작성할 수 있습니다.