React.memo
개념 설명: React.memo는 컴포넌트의 결과물을 메모이제이션(memoization)해 두었다가, 같은 props로 다시 렌더링하려고 하면 기존 결과를 재사용하는 기능입니다. 쉽게 말해 컴포넌트를 기억해두는 것이라고 할 수 있어요. 원래 리액트에서는 부모 컴포넌트가 업데이트되면 자식 컴포넌트도 다시 렌더링되는데, React.memo로 래핑한 컴포넌트는 전달된 props가 이전과 똑같다면 다시 렌더링하지 않고 넘어갑니다. (React.memo는 전달받은 props를 얕은(shallow) 비교를 통해 판단합니다.) 이런 방식으로 변경되지 않은 부분의 불필요한 재렌더링을 건너뛰어 성능을 최적화할 수 있습니다. (React.memo는 클래스 컴포넌트의 PureComponent와 비슷한 역할을 함수 컴포넌트에서 수행합니다.)
언제 사용해야 할까?: 컴포넌트 자체의 렌더링 비용이 크거나, 부모가 자주 다시 렌더링되어 변경되지 않은 자식 컴포넌트까지 반복 렌더링되는 경우에 React.memo를 고려할 수 있습니다. 예를 들어 리스트 아이템 컴포넌트나 복잡한 UI 컴포넌트가 있는데, 해당 컴포넌트의 props가 자주 바뀌지 않으면 React.memo로 감싸서 불필요한 업데이트를 방지할 수 있습니다. 단, 모든 컴포넌트에 무조건 React.memo를 쓰는 것은 좋지 않은데요. props가 자주 변한다면 매번 렌더링이 필요하므로 메모이제이션의 효과가 없고, 오히려 React.memo를 사용함으로써 props 비교에 약간의 오버헤드가 생길 수 있습니다. 따라서 실제로 렌더링 최적화가 필요한 경우에만 사용하는 것이 좋습니다.
예제 코드: 아래는 React.memo를 사용한 간단한 예제입니다. 부모 컴포넌트가 상태 업데이트로 재렌더링될 때, React.memo로 감싼 자식은 props가 변하지 않으면 재렌더링되지 않는 모습을 보여줍니다.
import React, { useState } from 'react';
// React.memo로 자식 컴포넌트를 메모이제이션
const Child = React.memo(({ data }) => {
console.log('🔄 Child 컴포넌트 렌더링');
return <p>Child: {data}</p>;
});
function Parent() {
const [count, setCount] = useState(0);
const data = "안녕하세요";
console.log('👨👧 Parent 컴포넌트 렌더링');
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
부모 state 증가
</button>
{/* Child 컴포넌트에 props로 data를 전달 */}
<Child data={data} />
</div>
);
}
위 코드에서 Parent 컴포넌트의 상태 count가 변경될 때마다 Parent는 다시 렌더링됩니다(👨👧 Parent 컴포넌트 렌더링 로그 출력). 이때 Child 컴포넌트도 일반적으로는 같이 렌더링되겠지만, Child를 React.memo로 감쌌기 때문에 data props가 변하지 않는 한 Child는 재렌더링되지 않습니다. data는 문자열 리터럴 "안녕하세요"로 고정되어 있으므로, 버튼을 눌러 count를 바꿀 때 Child 컴포넌트의 로그는 처음 한 번만 출력되고 이후에는 출력되지 않게 됩니다. 반대로 React.memo를 사용하지 않았다면 count 변화와 관계없이 Parent가 렌더링될 때마다 Child도 계속 렌더링되었을 것입니다.
정리: React.memo는 컴포넌트 단위의 메모이제이션 기법입니다. 변경되지 않은 props로 인해 발생하는 쓸데없는 컴포넌트 재렌더링을 건너뛰어 성능 향상을 얻을 수 있습니다. 렌더링 비용이 큰 컴포넌트나, 부모의 잦은 재렌더링으로 영향받는 컴포넌트에 적용하면 효과적입니다. 다만 props 변경 여부를 얕게 비교하므로 복잡한 객체를 props로 넘길 때는 예상과 다르게 동작할 수 있고, 꼭 필요한 경우에만 사용하는 것이 좋습니다.
useMemo
개념 설명: useMemo는 연산 결과를 메모이제이션하기 위한 React Hook입니다. 컴포넌트 함수 내부에서 어떤 값을 계산하는 함수를 호출하고 있다면, 일반적으로 컴포넌트가 다시 렌더링될 때마다 그 계산도 다시 이루어집니다. 그런데 매번 같은 결과가 나오는 무거운 계산이라면 비효율적이겠죠. useMemo를 사용하면 특정 값 계산을 수행한 결과를 기억해두었다가, 의존하는 값들이 바뀔 때만 다시 계산합니다. 한 마디로, 필요할 때만 계산하고 나머지 렌더링에서는 기존 값을 재사용하게 도와주는 것입니다. (useMemo는 첫 번째 인자로 계산 함수를 받고, 두 번째 인자로 의존성 배열을 받습니다. 의존성 배열에 있는 값들이 변경되지 않는 한, 이전에 계산한 값을 반환합니다.)
언제 사용해야 할까?: useMemo는 연산 비용이 큰 작업을 매 렌더링마다 반복하는 것을 피하고 싶을 때 사용합니다. 예를 들어 큰 배열을 정렬하거나 필터링하는 작업, 복잡한 수학 계산, 또는 반복문을 통해 무거운 처리를 하는 경우 등을 생각해볼 수 있습니다. 이러한 계산이 컴포넌트의 상태나 props가 바뀔 때 항상 다시 실행될 필요가 없다면, useMemo로 해당 결과를 캐싱하여 불필요한 재계산을 방지할 수 있습니다. 반면에 계산이 매우 간단하거나 거의 눈에 띄지 않는 수준이라면 useMemo의 효과가 크지 않을 수 있습니다. 또한 useMemo 자체도 메모리를 사용하고 약간의 오버헤드가 있으므로, 필요한 곳에 선별적으로 사용하는 것이 중요합니다 (무조건 남용하지 않아도 됩니다).
예제 코드: 아래 예제는 숫자를 두 배로 만드는 가정상 "비싼" 계산을 useMemo로 최적화한 모습입니다. 하나의 상태는 단순 카운터로 렌더링만 트리거하고, 다른 하나는 실제 계산에 사용됩니다. useMemo를 사용하여 계산 대상인 값(number)이 변경될 때만 연산을 실행하도록 합니다.
import React, { useState, useMemo } from 'react';
function App() {
const [count, setCount] = useState(0); // 단순 렌더링용 상태
const [number, setNumber] = useState(10); // 계산 대상 상태
// number가 바뀔 때에만 연산 수행
const doubled = useMemo(() => {
console.log('🔢 두 배 계산 중...');
return number * 2;
}, [number]);
return (
<div>
<p>계산 결과: {doubled}</p>
<button onClick={() => setCount(count + 1)}>
재렌더링하기
</button>
<button onClick={() => setNumber(number + 1)}>
number 변경하기
</button>
</div>
);
}
위 코드에서 doubled라는 값은 number * 2를 계산한 결과를 나타냅니다. 이 계산은 간단해 보이지만, 예시로서 console.log를 통해 언제 계산이 일어나는지 확인할 수 있습니다. count 상태를 변경하는 재렌더링 버튼을 누르면 App 컴포넌트는 다시 렌더링되지만, useMemo에 의해 number가 바뀌지 않는 한 '🔢 두 배 계산 중...' 로그는 출력되지 않습니다. 즉, doubled 값은 이전에 계산한 것을 그대로 사용합니다. 반면 number 변경 버튼을 누르면 number 값이 변하기 때문에 의존성 배열 [number]가 변경되고, 그때에만 계산을 수행하여 새로운 값을 얻습니다. 만약 useMemo를 쓰지 않고 doubled = number * 2로 했다면, count만 바뀌어도 매번 계산이 실행되었겠죠.
정리: useMemo는 값(연산 결과)의 메모이제이션을 위한 Hook으로, 렌더링 성능을 높이기 위해 불필요한 무거운 계산을 피할 수 있게 합니다. 전달한 함수의 결과값을 기억해 두고 **의존성(dependencies)**으로 지정한 값이 변경될 때만 함수를 다시 실행합니다. 그 결과 렌더링은 자주 되더라도 실제 계산은 필요한 순간에만 일어나도록 조절할 수 있습니다. 적절하게 사용하면 성능 최적화에 도움을 주지만, 남용하면 복잡도만 높아질 수 있으므로 꼭 필요할 때 활용하는 것이 좋습니다.
useCallback
개념 설명: useCallback은 함수(콜백)를 메모이제이션하기 위한 Hook입니다. React 컴포넌트가 렌더링될 때마다 내부에서 정의된 함수들은 매번 새롭게 생성됩니다. 대부분 이는 큰 문제가 아니지만, 생성된 함수들을 props로 자식 컴포넌트에 전달하는 경우에는 이야기가 달라집니다. 자식 입장에서는 전달받는 함수 prop이 매 렌더링마다 새로운 참조값을 가지므로, props가 변한 것으로 간주되어 불필요한 재렌더링이 발생할 수 있습니다. 또한 의존성 배열을 갖는 훅(ex: useEffect)에서 함수를 의존성으로 넣으면, 함수가 매번 새로 생성되면 의존성 변화로 감지되어 원하는 타이밍에만 동작하지 않을 수도 있습니다. useCallback은 이러한 문제를 해결하기 위해 함수 자체를 기억해둡니다. 특정 의존성 값들이 변하지 않는 한 같은 함수 객체를 반환하기 때문에, 앞서 말한 불필요한 업데이트를 줄일 수 있습니다. 쉽게 말해, useCallback은 함수를 위한 useMemo라고 볼 수 있습니다 (함수를 다시 만들지 않고 이전에 만든 걸 재사용한다는 면에서요).
언제 사용해야 할까?: 주로 자식 컴포넌트에 콜백 함수를 prop으로 전달할 때 useCallback을 사용합니다. 앞서 설명했듯이, 함수가 항상 새로 만들어지면 자식에서 props 변경으로 인한 재렌더링을 유발할 수 있으므로, 함수의 참조를 안정적으로 유지하기 위해 useCallback으로 감싸줍니다. 특히 자식 컴포넌트가 React.memo 등으로 최적화되어 있다면, 부모 쪽에서도 useCallback으로 함수를 메모이제이션해줘야 진짜로 재렌더링을 막을 수 있습니다. 또한 useEffect나 useMemo 등의 의존성 배열에 함수를 넣어야 하는 경우에도 함수가 불필요하게 재생성되지 않도록 useCallback을 사용할 수 있습니다. 반면에 함수형 업데이트를 사용하거나 (setCount(c => c+1)처럼) 함수가 아주 간단해서 재생성되어도 문제가 없으면 굳이 사용할 필요는 없습니다. 자식 컴포넌트의 성능 최적화가 필요할 때 또는 의존성 관리가 필요한 경우에 선택적으로 사용하세요.
예제 코드: 다음 예제는 부모가 자식에게 함수를 prop으로 넘기는 상황에서 useCallback의 효과를 보여줍니다. Child 컴포넌트는 React.memo로 감싸 최적화했고, 부모는 상태 변경을 위한 함수 increment를 정의하는데 useCallback을 사용했습니다.
import React, { useState, useCallback } from 'react';
// React.memo로 자식을 메모이제이션하여 props 변경시에만 렌더
const Child = React.memo(({ onClick }) => {
console.log('🔄 Child 렌더링');
return <button onClick={onClick}>증가</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// 빈 배열 []을 의존성으로 주어, 처음 한 번 생성 후 재사용
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
console.log('👨👧 Parent 렌더링');
return (
<div>
<p>Count: {count}</p>
{/* onClick prop으로 함수 전달 */}
<Child onClick={increment} />
</div>
);
}
이 코드에서 Parent 컴포넌트가 렌더링될 때마다 increment 함수를 새로 생성하면 (useCallback을 사용하지 않았다면) Child 컴포넌트는 매번 새로운 onClick prop을 받게 되어 React.memo임에도 불구하고 재렌더링될 것입니다. 하지만 increment를 useCallback으로 감싸고 의존성 배열을 [] (빈 배열)로 준 덕분에 컴포넌트 초기 마운트 시 한 번만 함수가 생성되고 이후에는 동일한 함수 객체를 재사용하게 됩니다. 따라서 Parent가 렌더링될 때 count 값 변화 이외에 Child에게 넘어가는 onClick prop은 변하지 않으므로, count가 증가해도 Child는 필요 이상으로 다시 렌더링되지 않습니다. (콘솔 로그를 보면 Parent는 상태 변경마다 출력되지만, Child는 처음 한 번만 출력됩니다.) 이처럼 useCallback과 React.memo를 함께 사용하면 부모-자식 간 렌더링 최적화를 극대화할 수 있습니다.
정리: useCallback은 함수의 메모이제이션을 위한 Hook으로, 동일한 함수 객체를 유지함으로써 자식 컴포넌트의 불필요한 재렌더링을 줄이고 의존성 배열 등의 안정성을 높입니다. 특히 함수를 props로 전달하는 상황에서 유용하며, React.memo와 함께 쓰이면 효과적입니다. 의존성 배열에 지정한 값들이 바뀔 때만 새로운 함수가 생성되므로, 그 외의 경우에는 이전에 만든 함수를 재사용하여 렌더링 횟수나 부수효과 트리거를 최소화합니다.
세 가지 기능 비교
위에서 살펴본 React.memo, useMemo, useCallback은 모두 리액트에서 성능을 최적화하기 위한 메모이제이션 도구라는 공통점이 있습니다. 하지만 각각 대상이 되는 범위나 사용 방법이 다르기 때문에 용도가 구분됩니다. 아래 표를 통해 세 가지의 차이점을 한눈에 비교해봅시다.
구분 | React.memo (컴포넌트) | useMemo (값) | useCallback (함수) |
종류 | HOC (고차 컴포넌트)(컴포넌트를 감싸는 함수) | Hook (함수 컴포넌트 내부에서 사용) | Hook (함수 컴포넌트 내부에서 사용) |
메모이제이션 대상 | 컴포넌트의 렌더링 결과(UI 출력물) | 계산된 값(리턴되는 변수) | 함수 정의 자체(이벤트 핸들러 등) |
사용 위치 | 컴포넌트 정의를 감싸서 export 또는 선언 시 적용 | 컴포넌트 함수 내부의 변수 선언 시 | 컴포넌트 함수 내부에서 함수 선언 시 |
효과 | 동일한 props이면 컴포넌트 재렌더링 방지 | 동일한 입력 값이면비싼 계산 재실행 방지 | 동일한 의존성이면 함수 재생성 방지 |
대표 사용 사례 | - 부모가 자주 렌더될 때, 자식이 불필요하게 렌더되는 경우- 무거운 컴포넌트의 반복 렌더링 최적화 | - 큰 배열이나 복잡한 계산 결과 캐싱- 연산 결과를 재사용해 렌더링 최적화 | - 콜백 함수를 자식에게 전달해 사용하는 경우- useEffect 등 훅 의존성에 함수 포함시 안정성 확보 |
각 기능의 핵심 차이는 위와 같습니다. 간단히 요약하면 React.memo는 컴포넌트를 메모이제이션하고, useMemo는 값을 메모이제이션하며, useCallback은 함수를 메모이제이션한다는 점이에요. 세 가지 모두 적절히 사용하면 불필요한 작업을 줄여 React 애플리케이션의 성능을 향상시킬 수 있습니다. 한 가지 알아둘 점은, 이러한 최적화 도구들은 애플리케이션의 특정 상황에서 성능 문제를 해결하기 위한 수단이지, 무조건 사용한다고 성능이 무조건 향상되는 것은 아니라는 것입니다. 따라서 언제나 성능을 모니터링하고, 실제로 병목이 되는 부분에 적용하는 것이 중요합니다.
'React' 카테고리의 다른 글
Page routing (0) | 2025.04.16 |
---|---|
React Context (0) | 2025.04.16 |
useReducer 동작 순서 (0) | 2025.04.15 |
useReducer (0) | 2025.04.15 |
useEffect로 라이프사이클 제어하는 법 (0) | 2025.04.15 |