언어·프레임워크/React.js
[React.js] React useCallback, 함수 인수는 어떻게 동작할까? (feat. 빈 의존성 배열 `[]`)
DandyNow
2025. 4. 4. 17:30
728x90
반응형
React useCallback, 함수 인수는 어떻게 동작할까?
React 개발 중 성능 최적화를 위해 useCallback
훅을 자주 사용하게 된다. 그런데 useCallback(fn, [])
처럼 빈 의존성 배열을 사용하면, 혹시 함수가 처음 호출될 때 사용된 인수가 고정되어 버리는 건 아닐까 하는 의문이 들 때가 있다. 마치 스냅샷처럼.
그래서 이 useCallback
과 함수 인수 전달의 관계에 대해 명확하게 알아고자 한다. 결론부터 말하면, 빈 의존성 배열은 인수를 고정시키지 않는다!
1. useCallback이란?
- 개념: 함수 자체를 기억(메모이제이션)하여, 특정 조건 하에서 함수의 재생성을 방지하는 React 훅이다.
- 목적: 주로 자식 컴포넌트에 props로 함수를 전달하거나
useEffect
의 의존성 배열에 함수를 포함할 때, 불필요한 리렌더링 또는useEffect
재실행을 막아 성능 최적화에 기여한다. - 동작:
useCallback(fn, deps)
형태로 사용한다.deps
배열 안의 의존성 값들이 변경되지 않는 한,useCallback
은 이전에 생성했던 동일한 함수 인스턴스를 반환한다.deps
가 빈 배열[]
이면, 컴포넌트가 처음 마운트될 때 단 한 번만 함수를 생성하고 이후에는 계속 그 함수를 재사용한다.
2. 흔한 오해: "빈 배열 []
이 인수를 고정시킨다?"
- 가끔
useCallback(fn, [])
을 쓰면,fn
함수가 처음 호출될 때 사용된 인수(예:fn('첫번째 인수')
)가 그대로 기억되어, 나중에fn('두번째 인수')
처럼 다른 인수로 호출해도 계속 '첫번째 인수'가 사용될 거라고 생각하는 경우가 있다. - 하지만 이것은 잘못된 이해이다. useCallback은 함수 호출 시점의 인수를 저장하지 않는다.
3. 실제 동작 방식과 예제 코드
useCallback
은 함수 정의(코드 자체)를 메모리에 저장해두고 재사용하는 것이다.- 메모이제이션된 함수가 호출되는 시점에 어떤 인수가 전달되었는지에 따라, 함수 내부에서는 그때 전달된 인수를 사용한다.
- 예제 코드를 통해 직접 확인해 보자!
import React, { useState, useCallback, useEffect } from 'react';
function CallbackArgDemo() {
const [text, setText] = useState('');
// useCallback으로 메모이제이션된 함수 (인수: prefix)
// 빈 의존성 배열을 사용 -> 이 함수 인스턴스는 한 번만 생성됨
const logMessage = useCallback((prefix) => {
// 함수 본문에서는 호출 시점에 전달된 'prefix' 인수를 사용함
// text 상태는 사용하지 않음 (인수 전달에 집중하기 위함)
console.log(`[Callback] ${prefix}: 이 메시지는 useCallback 함수로부터 왔습니다.`);
}, []); // 의존성 없음
// 비교: useCallback 없이 정의된 함수
const logMessagePlain = (prefix) => {
// 이 함수는 리렌더링될 때마다 새로 생성됨
console.log(`[Plain] ${prefix}: 이 메시지는 일반 함수로부터 왔습니다.`);
};
console.log("--- 컴포넌트 리렌더링 ---");
// useEffect에서 logMessage 함수 참조 (참조 안정성 테스트용)
useEffect(() => {
console.log("logMessage 함수 참조 변경됨 (useCallback)");
// 이 로그는 logMessage 함수 참조가 변경될 때만 출력됨
// 빈 배열 useCallback 덕분에 마운트 시 1회만 출력될 것임
}, [logMessage]);
useEffect(() => {
console.log("logMessagePlain 함수 참조 변경됨 (Plain)");
// 이 로그는 리렌더링 시마다 매번 출력될 것임
}, [logMessagePlain]);
return (
<div>
<h1>useCallback 인수 전달 테스트</h1>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="리렌더링 유발용 입력창"
/>
<hr />
<h2>useCallback 함수 호출</h2>
{/* 동일한 logMessage 함수를 다른 인수로 호출 */}
<button onClick={() => logMessage('INFO')}>정보 로그 (Callback)</button>
<button onClick={() => logMessage('WARN')}>경고 로그 (Callback)</button>
<hr />
<h2>일반 함수 호출</h2>
<button onClick={() => logMessagePlain('INFO')}>정보 로그 (Plain)</button>
<button onClick={() => logMessagePlain('WARN')}>경고 로그 (Plain)</button>
</div>
);
}
export default CallbackArgDemo;
4. 예제 코드 분석
CallbackArgDemo
컴포넌트는 입력(text
) 상태를 가지고 있어, 입력할 때마다 리렌더링이 발생한다.logMessage
함수는useCallback(..., [])
으로 정의되어, 컴포넌트가 처음 렌더링될 때 단 한 번만 생성된다. 이후 리렌더링 시에도 동일한 함수 인스턴스가 유지된다. (useEffect
로그 참조)logMessagePlain
함수는useCallback
없이 정의되어, 컴포넌트가 리렌더링될 때마다 매번 새로운 함수 인스턴스가 생성된다. (useEffect
로그 참조)- 핵심 포인트:
- "정보 로그 (Callback)" 버튼과 "경고 로그 (Callback)" 버튼은 동일한
logMessage
함수 인스턴스를 호출한다. - 하지만 버튼을 클릭할 때 각각 다른
prefix
인수('INFO', 'WARN')를 전달한다. logMessage
함수는 호출될 때 전달받은 그prefix
인수를 사용하여 콘솔에 로그를 정확히 출력한다. 이는useCallback
이 인수를 고정하지 않음을 보여준다.
- "정보 로그 (Callback)" 버튼과 "경고 로그 (Callback)" 버튼은 동일한
5. 그럼 useCallback은 왜 사용할까?
- 주된 이유는 참조 안정성(Referential Stability) 확보이다.
- 언제 유용한가?
React.memo
로 감싸진 자식 컴포넌트에 함수를 props로 내려줄 때: 함수 참조가 동일하게 유지되므로 자식 컴포넌트의 불필요한 리렌더링을 방지할 수 있다.useEffect
의 의존성 배열에 함수를 포함할 때: 함수 참조가 변경되지 않으면useEffect
가 불필요하게 재실행되는 것을 막을 수 있다. (예제 코드의useEffect
참고)
6. 결론
useCallback(fn, [])
은 함수fn
의 정의를 메모이제이션하여 참조 안정성을 제공하는 것이지, 함수 호출 시의 인수를 저장하거나 고정하는 것이 아니다.- 메모이제이션된 함수도 일반 함수와 마찬가지로, 호출될 때 전달받는 인수를 사용하여 동작한다.
- 그러니 안심하고
useCallback
으로 메모이제이션된 함수에 다양한 인수를 전달하여 사용해 보자!
728x90
반응형