React useCallback: 같은 제출 함수가 느린 자식을 다시 그리게 할 때
테마 변경 때 새 함수 prop 때문에 memo 자식이 다시 Render되는지 측정하고, 필요한 동일성 경계에서만 callback 참조를 재사용한다.
목차
표준·구현·측정·해석 표시는 무엇인가요?
- 표준웹 표준이나 언어 명세가 정한 동작
- 구현특정 기술이나 브라우저가 실제로 구현한 동작
- 측정명시한 환경에서 직접 실행해 관찰한 결과
- 해석앞선 근거에서 도출한 설계 판단
- 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용
30초 요약
useCallback은 함수 실행 결과가 아니라 함수 참조를 재사용하는 성능 최적화다. 새 함수를 만드는
일 자체를 피하려고 모든 함수에 넣지 않는다. memo 자식의 prop이나 다른 Hook의 의존성처럼 함수
동일성을 실제로 비교하는 경계가 있고, 그 경계의 Render가 느리다는 측정이 있을 때 검토한다.
이 글을 관통하는 상황: 테마만 바꿨는데 배송 폼도 느리다
다음 예제의 반복문은 느린 자식 Render를 재현하기 위한 인위적 실험이다. 실제 서비스에 복사하지
않는다. console.count도 Render 횟수를 잠시 확인하는 진단이므로 측정 뒤 제거한다.
import { memo, useState } from 'react'
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
console.count('ShippingForm Render: 임시 진단')
let checksum = 0
for (let index = 0; index < 30000000; index += 1) {
checksum += index % 7
}
const [address, setAddress] = useState('')
function handleSubmit(event) {
event.preventDefault()
onSubmit({ address })
}
return (
<form onSubmit={handleSubmit}>
<input
value={address}
onChange={event => setAddress(event.target.value)}
aria-label="배송 주소"
/>
<button type="submit">주문</button>
<output>진단값: {checksum}</output>
</form>
)
})
function postOrder(productId, referrer, orderDetails) {
console.log('주문 전송', { productId, referrer, orderDetails })
}
function ProductPage({ productId, referrer }) {
const [isDark, setIsDark] = useState(false)
const [lastOrder, setLastOrder] = useState('없음')
function handleSubmit(orderDetails) {
postOrder(productId, referrer, orderDetails)
setLastOrder(`${productId} / ${orderDetails.address}`)
}
return (
<section data-theme={isDark ? 'dark' : 'light'}>
<p>현재 상품: {productId}</p>
<p>현재 테마: {isDark ? '어두움' : '밝음'}</p>
<p>마지막 전송: {lastOrder}</p>
<button type="button" onClick={() => setIsDark(value => !value)}>
테마 변경
</button>
<ShippingForm onSubmit={handleSubmit} />
</section>
)
}
export default function App() {
const [productId, setProductId] = useState('coffee')
return (
<main>
<select value={productId} onChange={event => setProductId(event.target.value)}>
<option value="coffee">커피</option>
<option value="tea">차</option>
</select>
<ProductPage productId={productId} referrer="home" />
</main>
)
}테마 버튼을 누르면 ProductPage가 다시 Render된다. productId와 referrer는 그대로지만
ShippingForm Render 수가 늘고 인위적 반복 계산 때문에 테마 반응도 늦어진다. ShippingForm을
memo로 감쌌는데도 onSubmit prop이 매 Render마다 다른 함수이기 때문이다.
먼저 프로젝트 설정과 컴파일 결과를 확인하고, 테마 변경 때 자식 Render가 실제로 반복되는 환경에서만
아래 수동 비교를 진행한다. 반복되지 않는다면 수동 useCallback을 추가하지 않는다.
같은 코드의 함수도 Render마다 다른 값이다
React Profiler로 테마 변경 update를 기록해 ProductPage와 ShippingForm이 함께 Render됐는지,
ShippingForm의 Render 비용이 전체 지연에서 의미 있는지 먼저 확인한다.
callback이 읽는 값을 의존성으로 둔다
측정에서 새 onSubmit 참조가 느린 자식 Render를 깨운다고 확인했다면 부모를 다음처럼 교정한다.
import { useCallback, useState } from 'react'
function ProductPage({ productId, referrer }) {
const [isDark, setIsDark] = useState(false)
const [lastOrder, setLastOrder] = useState('없음')
const handleSubmit = useCallback(orderDetails => {
postOrder(productId, referrer, orderDetails)
setLastOrder(`${productId} / ${orderDetails.address}`)
}, [productId, referrer])
return (
<section data-theme={isDark ? 'dark' : 'light'}>
<p>현재 상품: {productId}</p>
<p>현재 테마: {isDark ? '어두움' : '밝음'}</p>
<p>마지막 전송: {lastOrder}</p>
<button type="button" onClick={() => setIsDark(value => !value)}>
테마 변경
</button>
<ShippingForm onSubmit={handleSubmit} />
</section>
)
}
export default function App() {
const [productId, setProductId] = useState('coffee')
return (
<main>
<select value={productId} onChange={event => setProductId(event.target.value)}>
<option value="coffee">커피</option>
<option value="tea">차</option>
</select>
<ProductPage productId={productId} referrer="home" />
</main>
)
}productId와 referrer가 같으면 테마 변경 뒤에도 onSubmit 참조가 같아 ShippingForm이 부모 때문에
다시 Render되는 일을 건너뛸 수 있다. 둘 중 하나가 바뀌면 현재 주문 정보를 읽는 새 함수를 받아야
하므로 자식도 다시 Render된다.
의존성을 빈 배열로 바꾸면 더 강한 최적화가 되는 것이 아니다. 최초 Render의 productId와
referrer를 계속 읽는 오래된 클로저가 되어 다른 상품 주문을 잘못 전송할 수 있다.
함수 참조를 소비하는 경계가 있는지 먼저 본다
현재 React 19.2는 개발 중 컴포넌트 파일을 편집하거나 컴포넌트가 초기 화면 반영 중 필요한 코드·
데이터를 기다리며 suspend되면 useCallback 캐시를 버릴 수 있다. 따라서 의존성이 같아도 함수
참조가 영구히 유지된다는 기능 계약에 의존하지 않는다.
다음 조건이 없다면 함수 선언을 그대로 두는 편이 단순하다.
- 측정상 느린 자식이
memo로 props 비교를 하고 함수 prop 하나 때문에 비교가 실패한다. - 함수가 다른 Hook의 의존성이고 함수 동일성 변화가 외부 동기화를 불필요하게 다시 시작한다.
useCallback은 함수 본문 실행을 건너뛰지 않는다. 사용자가 주문 버튼을 누르면 함수는 정상 실행된다.
함수 정의를 캐시하는 비교·보관 비용도 있으므로 “함수가 보인다”는 이유만으로 추가하지 않는다.
Effect만 쓰는 함수는 Effect 안으로 옮길 수 있다
5편의 ChatRoom에서 선택한 방의 연결 정보를 계산하는 보조 함수가 오직 연결 Effect에서만 쓰인다고
하자. 그 함수 정의를 Effect 안으로 옮기면 함수 자체는 의존성이 아니며, 실제로 연결을 다시 시작해야
하는 roomId만 의존성에 남는다. 함수가 자식 prop으로도 전달되는 등 Effect 밖의 동일성 경계가
있다면 그때 useCallback을 검토한다.
같은 테마 상호작용을 전후로 비교한다
교정 전후를 같은 Compiler 설정과 기기에서 비교한다.
- 테마 이름이
밝음 → 어두움으로 정확히 바뀌는지 확인한다. - 교정 전에는 테마 변경 때
ShippingFormRender가 기록되는지 확인한다. - 교정 뒤에는 같은 변경에서 자식 Render가 건너뛰어지는지 확인한다.
- 주소 입력처럼 자식 자신의 상태가 바뀌면 여전히 자식이 Render되는지 확인한다.
productId를 바꾸면 새 제출 함수와 함께 자식이 Render되고 올바른 상품이 전송되는지 확인한다.
문제 테마만 바꿔도 느린 ShippingForm이 다시 Render됨
잘못된 판단 memo가 있으면 함수 prop 내용이 같아도 자식 Render를 건너뜀
관찰 handleSubmit !== previousHandleSubmit, ShippingForm Render 비용 반복
원인 부모 Render마다 새 함수 참조가 onSubmit prop으로 전달됨
행동 productId와 referrer 의존성으로 handleSubmit 참조 재사용
검증 테마 변경은 자식 생략, 주소·상품 변경은 필요한 Render와 제출 유지풀 리퀘스트에서는 다음을 확인한다.
- 느린 자식 Render가 측정됐고
memo가 실제로 적용돼 있는가? - 함수 참조가 달라지는 것이 props 비교 실패의 직접 원인인가?
- callback이 읽는 모든 반응형 값이 의존성에 있는가?
- 함수가 Effect 안으로 이동할 수 있는데 불필요하게 메모화하지 않았는가?
- Compiler가 이미 최적화한 경계에 수동 코드를 중복하지 않았는가?
- 같은 상호작용으로 기능 결과와 시간을 다시 확인했는가?
기억할 질문은 하나다.
이 함수 참조를 실제로 비교하는 느린 경계가 있는가?
다음 글에서는 공용 컴포넌트가 내부 DOM이나 Native 구조가 아니라 사용 의도를 props 계약으로 표현해야 하는 이유를 살펴본다.
Active recall
기억에서 꺼내 보기
답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.
01productId와 referrer가 그대로인데 테마 변경 때 memo ShippingForm이 다시 Render되는 이유는 무엇인가?
정답
부모 Render마다 새 handleSubmit 함수가 만들어져 onSubmit prop의 객체 동일성이 달라지기 때문이다.
관련 설명 다시 읽기왜 그런가
memo는 각 prop을 이전 값과 Object.is로 비교하므로 함수 내용이 같아도 새 함수 참조는 다르다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
02handleSubmit이 productId와 referrer를 읽는다면 useCallback 의존성은 무엇이어야 하는가?
정답
productId와 referrer다. 둘이 같을 때 이전 함수 참조를 받고 하나라도 달라지면 현재 Render의 새 함수를 받는다.
관련 설명 다시 읽기왜 그런가
callback이 읽는 반응형 값을 빼면 이전 주문 대상이나 유입 정보를 사용하는 오래된 closure가 될 수 있다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
03자식이 memo가 아니고 함수가 다른 Hook 의존성도 아니라면 useCallback을 추가할 실질적 이점이 있는가?
정답
보통 없다. 함수 생성 자체는 문제가 아니며 실제로 참조 재사용을 소비하는 동일성 경계가 있어야 한다.
관련 설명 다시 읽기왜 그런가
useCallback은 함수 실행을 막지 않고 함수 참조를 재사용하는 최적화다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
04함수가 오직 Effect 안에서만 쓰인다면 useCallback 전에 어떤 단순화를 검토할 수 있는가?
정답
함수를 Effect 안으로 옮겨 함수 자체를 의존성에서 제거하고 실제 반응형 값만 의존성으로 둘 수 있다.
관련 설명 다시 읽기왜 그런가
동일성 문제를 만들지 않는 구조가 함수 메모화보다 단순하고 캐시 폐기에도 영향을 받지 않는다.
선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.
출처와 검증 범위
아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.
- useCallbackReact · 공식 문서 · 확인 2026-08-09
- memoReact · 공식 문서 · 확인 2026-08-09
- Removing Effect DependenciesReact · 공식 문서 · 확인 2026-08-09
- React Compiler IntroductionReact · 공식 문서 · 확인 2026-08-09