리액트 컴포넌트는 왜 리렌더링될까?
React 함수 컴포넌트는 사실 "그냥 함수"입니다.
마운트가 되면 모든 함수는 리렌더링이 일어나는데요, 리렌더링이 일어나면, 그 함수가 처음부터 다시 실행됩니다.
- 컴포넌트 안에 있는 변수 선언, 함수 선언, 연산 → 전부 다시 실행됩니다.
리렌더링(함수 컴포넌트가 다시 실행 됨) 할 때 마다 모든 계산이 새로 실행되면, 메모리 과부하. 성능 낭비가 올 수 있겠죠
성능 낭비를 막을 수 있는 훅 중 가장 유명한 훅 두개를 파해쳐 볼게요
useMemo : 연산 결과를 캐싱해서 필요할 때만 다시 계산, 자식 렌더 차단
useCallback : 함수 인스턴스를 캐싱해서 필요할 때만 새로 만듦
UseMemo 부터 어떻게 생겼는지, 어떤 쓰임새로 쓰는지 알아봅시다
useMemo란 무엇인가?
useMemo
- useMemo의 의의는 이렇습니다. 리렌더될 때마다 매번 다시 계산하지 말고, 값 계산 결과를 기억해 두자!
useMemo(() => {
return 1 + 2; // 결과: 3
}, []);
여기서 저는 [] 이 괄호의 존재이유가 궁금했는데요, 이두 번째 인자: [] (의존성 배열, deps array)
→ 언제 이 계산 함수를 다시 실행할지를 결정하는 친구입니다.
- [] → 처음 마운트될 때 딱 한 번만 실행. 이후에는 캐싱된 값 재사용.
- [data] → data가 바뀔 때만 다시 실행.
- [a, b] → a나 b가 바뀔 때만 다시 실행.
useMemo는 "계산을 기억해두자"인데, 언제 다시 계산해야 하는지 기준이 필요합니다. []는 아무것도 의존하지 않으니, 처음 한 번만 계산하고 이후에는 절대 다시 안 한다는 뜻입니다.
useMemo(() => 계산, [값들]) = "이 값들이 바뀔 때만 계산을 다시 해라"
이제 예시를 볼게요! useMemo를 쓰지 않은 경우 먼저 봅시다.
리액트는 컴포넌트가 리렌더될 때마다 함수 안 코드를 처음부터 다시 실행해요.
그래서 이런 상황이 생깁니다:
const numbers = Array.from({ length: 100000 }, () => Math.random());
const sorted = numbers.sort((a, b) => a - b);
- 버튼 하나 눌러도 다시 10만 개 배열 만들고 정렬. (굉장히 불필요하죠)
- 이는 성능 낭비로 이어집니다.
useMemo를 써볼까요?
const sorted = useMemo(() => {
// 무거운 계산
const numbers = Array.from({ length: 100000 }, () => Math.random());
return numbers.sort((a, b) => a - b);
}, []);
1. () => { ... } 안의 계산을 실행해서 나온 값을 기억해둠
2. 뒤에 [] → 의존성이 없으니 처음 한 번만 계산하고 재사용
3. 값이 필요할 때마다 sorted를 꺼내 쓰면 됨
그렇다면 언제쓰는지 상황을 정리해봅시다.
useMemo는 언제 쓰면 좋을까?
- 비싼 계산 결과 캐싱 (정렬, 필터링, 파싱 등)
- 객체/배열 props 안정화
- 부모가 자식에 props로 [1,2,3] 같은 배열을 내려줄 때
- 매번 새 배열이 생기지 않도록 useMemo로 고정
정리를 하면
- useMemo 안 쓰면 → 리렌더마다 계산 다시 함 → 성능 낭비
- useMemo 쓰면 → 필요한 시점(의존성 변경)만 계산 → 효율적
useCallback이란 무엇인가?
useCallback
- useCallback의 의의는 이렇습니다. 리렌더될 때마다 함수가 새로 만들어지는 걸 막고, 같은 함수 참조를 유지하자!
자 그럼 예시 코드를 봅시다. 부모(App) 컴포넌트에서 숫자 카운트를 올리는 버튼과, 자식(SearchBox) 컴포넌트에서 검색어를 입력받아 로그로 찍는 예제 코드입니다
import { useState } from "react";
function SearchBox({ onSearch }: { onSearch: (q: string) => void }) {
console.log("🔍 SearchBox 렌더링됨");
return (
<div>
<input
placeholder="검색어 입력"
onChange={(e) => onSearch(e.target.value)}
/>
</div>
);
}
export default function App() {
const [count, setCount] = useState(0);
// ❌ 매 렌더마다 새로운 함수 생성됨
const handleSearch = (q: string) => {
console.log("검색 실행:", q);
};
return (
<div>
<button onClick={() => setCount(c => c + 1)}>카운트: {count}</button>
<SearchBox onSearch={handleSearch} />
</div>
);
}
이 코드의 문제점은, 부모 App이 리렌더될 때마다 handleSearch 새로 생성된다는 것이죠. SearchBox는 (기본적으로) 부모 리렌더 때 같이 리렌더 됩니다. 그렇다면 useCallback을 써서 효율적으로 만들어볼까요?
import { useState, useCallback } from "react";
function SearchBox({ onSearch }: { onSearch: (q: string) => void }) {
console.log("🔍 SearchBox 렌더링됨");
return (
<div>
<input
placeholder="검색어 입력"
onChange={(e) => onSearch(e.target.value)}
/>
</div>
);
}
export default function App() {
const [count, setCount] = useState(0);
// ✅ useCallback으로 함수 참조 고정
const handleSearch = useCallback((q: string) => {
console.log("검색 실행:", q);
}, []); // [] → 의존성이 없으므로 처음 한 번만 생성
return (
<div>
<button onClick={() => setCount(c => c + 1)}>카운트: {count}</button>
<SearchBox onSearch={handleSearch} />
</div>
);
}
적용을 했죠?
useCallback을 이용해 handleSearch 함수 참조를 고정시켰으나, SearchBox는 여전히 리렌더됩니다.
왤까요?
React.memo가 아니면 여전히 리렌더되기 때문입니다. 기본 함수형 컴포넌트는 부모가 리렌더되면 함께 렌더됩니다. 렌더 스킵은 React.memo의 역할이에요.React.memo는 props가 이전과 같으면 컴포넌트를 아예 다시 그리지 않습니다.
그러나 usecallback은 같은 함수참조만 유지하니까요.
정리하자면,
- “렌더를 막고 싶다” → React.memo의 역할
- “함수 props 때문에 props가 매번 달라진다” → 부모에서 useCallback
- “값 props(객체/배열) 때문에 달라진다” → useMemo 또는 프리미티브 분해
자 그럼 이렇게 해볼까요? useCallback 과 memo를 조합해 써봅시다.
import { useState, useCallback, memo } from "react";
const SearchBox = memo(function SearchBox({ onSearch }: { onSearch: (q: string) => void }) {
console.log("🔍 SearchBox 렌더링됨");
return (
<div>
<input
placeholder="검색어 입력"
onChange={(e) => onSearch(e.target.value)}
/>
</div>
);
});
export default function App() {
const [count, setCount] = useState(0);
// ✅ useCallback: 함수 참조 고정
const handleSearch = useCallback((q: string) => {
console.log("검색 실행:", q);
}, []); // deps가 생기면 반드시 의존성에 넣기!
return (
<div>
<button onClick={() => setCount(c => c + 1)}>카운트: {count}</button>
<SearchBox onSearch={handleSearch} />
</div>
);
}
- 카운트 버튼을 눌러 App이 리렌더돼도 handleClick 참조가 그대로 → Child의 props가 변하지 않음
- Child는 "🔍 Child 렌더링됨"이 처음 한 번만 찍히고 끝 !
그런데 과연, useMemo가 필요했을까요?
useMemo는 값(배열, 객체, 숫자 계산 결과 등)을 기억해두는 도구예요.
“렌더링이 다시 일어나도, 값 계산을 다시 하지 말고 지난번 값을 써라” → 이게 핵심임다.여기서 SearchBox는 그냥 컴포넌트 정의예요. 값 캐싱할 게 없습니다. input 하나 렌더하는데 굳이 useMemo로 “이걸 기억해!” 할 이유가 없어요.
useMemo vs useCallback, 어떻게 써야 할까?
결국, 정리하자면,useMemo는 값 계산을 캐싱하고, useCallback은 함수 참조를 캐싱한다는 차이가 있습니다.
둘 다 목적은 단순합니다. 바로 불필요한 리렌더링과 재계산을 막아 성능을 지키는 것이죠.
다만, 모든 곳에 무조건 붙이는 게 답은 아닙니다. 연산이 가벼운 경우나 컴포넌트 구조가 단순한 경우엔 오히려 코드만 복잡해질 수 있습니다.
따라서 꼭 필요한 지점에서만 — 예를 들어 리스트형 UI, 무거운 연산, 자식 컴포넌트에 props를 내릴 때 — 신중하게 사용한다면, 애플리케이션의 성능과 가독성을 동시에 챙길 수 있습니다.
“측정하고, 필요한 곳에만 최적화한다”
이것이 useMemo와 useCallback을 올바르게 활용하는 가장 중요한 원칙이 되겠습니다