memo, useMemo, useCallback은 분명 성능 최적화를 위한 도구이지만, “언제 써야 하는지” 감이 안 잡히는 경우가 많습니다. 성능 최적화 도구이지만, 정확한 원리를 모르면 오히려 독이 될 수 있으니 잘 알고 써야 합니다! 리렌더링의 원리부터 차근차근 살펴볼게요.
React에서 렌더링은 컴포넌트 함수가 다시 실행되는 것을 의미해요. 그렇다면 언제 리렌더링은 어떤 상황에서 발생할까요? 주로 다음과 같은 4가지 상황에서 발생합니다.
가장 대표적인 경우입니다. 리액트에서 useState로 만든 “상태”가 변경되면 컴포넌트는 바뀐 상태 정보를 보여주기 위해 리렌더링 됩니다.
export default function App() {
const [count, setCount] = useState(0)
return (
// 클릭 시 리렌더링
<button onClick={() => setCount((prev) => prev + 1)}>버튼</button>
)
}부모로부터 전달받는 데이터가 달라지면 자식 컴포넌트도 리렌더링 됩니다.
export default function App() {
const [count, setCount] = useState(0)
return (
<>
<Child count={count} />
// 클릭 시 Child 리렌더링
<button onClick={() => setCount((prev) => prev + 1)}>버튼</button>
</>
)
}버튼을 누르면 count 값이 변경되면서 Child도 바뀌게 됩니다.
props가 안 바뀌어도 부모가 리렌더링되면 자식도 기본적으로 함께 리렌더링 됩니다.
export default function App() {
const [count, setCount] = useState(0)
return (
<>
<Child label="고정 라벨" />
// 클릭 시 Child 리렌더링
<button onClick={() => setCount((prev) => prev + 1)}>버튼</button>
</>
)
}useContext로 구독하고 있는 값이 바뀌면 관련 컴포넌트들이 모두 리렌더링 됩니다.
// CountProvider, Count, Button → 리렌더링
<CountProvider>
<CountConsumer />
<Button />
<NotConsumer /> // Context를 구독하지 않는 컴포넌트라면 리렌더링 X
</CountProvider>리렌더링 조건 3번에서, 부모가 리렌더링되면 자식도 기본적으로 함께 리렌더링된다고 했습니다. 그런데, 화면에 변화가 없는 자식 컴포넌트까지 굳이 다시 그려질 필요는 없겠죠? 이때 memo를 사용하면 Props가 변했을 때만 다시 리렌더링되도록 할 수 있습니다.
export default function App() {
const [count, setCount] = useState<number>(0)
const handleButtonClick = () => {
setCount((prev) => prev + 1)
}
return (
<div>
<Count count={count} />
<CustomButton onClick={handleButtonClick}>버튼</CustomButton>
</div>
)
}
function Count({ count }: { count: number }) {
return <div>버튼을 누른 횟수는 {count}회</div>
}
const CustomButton = function CustomButton(props: ComponentProps<'button'>) {
return <button {...props} />
}
위의 App 컴포넌트에서는 버튼을 누를 때마다, count 상태가 변하게 되고, 상태가 변했으니 <App />도 리렌더링 됩니다. 따라서 <Count />와 <CustomButton /> 또한 부모가 렌더링됨에 따라 리렌더링 되죠.

이때, <Count />와 달리 <CustomButton />은 props도 바뀌지 않고, 화면 상에서도 바뀌지 않기 때문에 리렌더링 될 필요 없습니다. 따라서, memo로 감싸줍시다.
const CustomButton = memo(function CustomButton(
props: ComponentProps<'button'>,
) {
return <button {...props} />
})그런데 이상한 점은 계속해서 버튼이 렌더링 된다는 것입니다. 왜일까요? 원인은 바로 함수의 참조값 때문입니다.
<App />이 함수가 다시 실행될 때마다 내부에 선언된 handleButtonClick 함수가 새롭게 만들어집니다. 참조값이 달라진 함수는 React에서 새로운 함수로 인식되고 onClick props도 바뀐 것으로 간주됩니다.
이때 useCallback으로 함수를 감싸주면, 해당 함수는 리렌더링이 발생해도 기존 참조를 유지하기 때문에 불필요한 리렌더링을 방지할 수 있습니다.
const handleButtonClick = useCallback(() => {
console.log('버튼 클릭')
setCount((prev) => prev + 1)
}, [])
결론부터 말씀드리면 "아니요"입니다. 최적화 도구 사용에도 비용이 따르기 때문이에요.
memo나 useCallback은 이전 값을 저장해두고 매번 비교하는 과정을 거칩니다. 오히려 단순한 컴포넌트에서는 이 비교 연산이 렌더링보다 더 무거울 수 있어요.
✅ 이럴 때 사용하는 것을 추천드려요!
memo를 적용한 자식에게 함수나 객체를 Props로 넘겨줄 때useEffect 등의 의존성 배열에서 함수의 동일성을 보장해야 할 때꼭 필요한 곳에 적절히 사용하여 성능까지 잡는 개발자가 됩시다! ^0^