재렌더링이 발생하는 조건
리액트에서 재렌더링을 적절하게 제어하기 위해서는 재렌더링이 언제 발생하는지 알아야 합니다.
재렌더링이 발생하는 세 가지 패턴
재렌더링이 발생하는 조건은 다음과 같이 세 가지입니다.
1. State가 업데이트된 컴포넌트
2. Props가 변경된 컴포넌트
3. 재렌더링된 컴포넌트 아래의 모든 컴포넌트
1, 2번 조건은 쉽게 떠올릴 수 있을 것입니다. 1번의 State는 컴포넌트 상태를 나타내는 변수입니다. 업데이트될 때 재렌더링되지 않으면 화면 표시를 올바르게 저장할 수 없습니다. 지금까지 본 것처럼 카운트업 변수를 실행해서 State를 변경하면 화면에 변경한 값이 실시간으로 반영되는 것은 State가 업데이트됨에 따라 컴포넌트가 재렌더링되기 때문입니다.
2번의 경우, 리액트 컴포넌트는 Props를 인수로 받고 그에 대응해 렌더링 내용을 결정하므로 Props의 값이 변할 때는 재렌더링해서 출력 내용을 변경해야 합니다. 따라서 Props값이 변할 때는 반드시 재렌더링을 합니다.
주의할 것은 3번입니다. 입문자가 간과하기 쉬운 포인트입니다. '재렌더링된 컴포넌트 아래의 모든 컴포넌트'에 관해 다음과 같은 파일 구성을 가정해서 설명하겠습니다.

컴포넌트 계층 구조는 다음과 같습니다.

컴포넌트 트리가 이러할 때 재렌더링된 컴포넌트 아래의 모든 컴포넌트가 렌더링된다는 것은, 루트 컴포넌트인 App.jsx가 State를 변경한 경우 모든 컴포넌트가 재렌더링된다는 것을 의미합니다.

마찬가지로 Child1.jsx가 State를 변경하는 경우는 다음과 같이 재렌더링됩니다.

이렇게 자녀 컴포넌트는 특히 Props가 변경되지 않았더라도 디폴트 상태에서는 부모가 재렌더링되면 함께 재렌더링됩니다. 표시가 달라지지 않는데 매번 불필요한 렌더링을 하는 것은 성능 저하를 일으키는 원인이 되므로 그런 상황이 발생하지 않도록 코드에서 재렌더링을 최적화합니다.
렌더링 최적화1(memo)
코드 기반으로 재렌더링을 제어하는 방법을 알아보겠습니다.
사전 준비
아래와 같이 프로젝트를 작성합니다. App.jsx에서는 카운트업 기능을 구현합니다. 또한 각 컴포넌트가 재렌더링되는 것을 시각적으로 쉽게 알 수 있도록 함수 컴포넌트에 console.log를 추가해둡니다.

- 사전 준비(App.jsx)
import { useState } from "react";
import { Child1 } from "./components/Child1";
import { Child4 } from "./components/Child4";;
export const App = () => {
console.log("App 렌더링");
const [num, setNum] = useState(0);
const onClickButton = () => {
setNum(num+1);
};
return(
<>
<button onClick={onClickButton}>버튼</button>
<p>{num}</p>
<Child1 />
<Child4 />
</>
);
};
- 사전 준비(Child1.jsx)
import { Child2 } from "./Child2";
import { Child3 } from "./Child3";
const style = {
height: "200px",
backgroundColor: "lightblue",
padding: "8px"
};
export const Child1 = () => {
console.log("Child1 렌더링");
return (
<div style={style}>
<p>Child1</p>
<Child2 />
<Child3 />
</div>
);
};
- 사전 준비(Child2.jsx)
const style = {
height: "50px",
backgroundColor: "Lightgray"
};
export const Child2 = () => {
console.log("Child2 렌더링");
return(
<div style={style}>
<p>Child2</p>
</div>
);
};
- 사전 준비(Child3.jsx)
const style = {
height: "50px",
backgroundColor: "lightgray"
};
export const Child3 = () => {
console.log("Child3 렌더링");
return(
<div style={style}>
<p>Child3</p>
</div>
);
};
- 사전 준비(Child4.jsx)
const style = {
height: "200px",
backgroundColor: "wheat",
padding: "8px"
};
export const Child4 = () => {
console.log("Child4 렌더링");
return(
<div style={style}>
<p>Child4</p>
</div>
);
};
아래와 같이 표시됩니다.

App의 자녀 컴포넌트로서 Child1과 Child4 그리고 Child1의 자녀 컴포넌트로 Child2, Child3을 설정한 상태입니다. 이 상태에서 App의 카운트업을 실행하면 App의 State가 변경됨에 따라 모든 컴포넌트가 재렌더링되는 것을 콘솔에서 확인할 수 있습니다.


이때 App 이외의 컴포넌트는 표시가 변하는 것이 아니니 재렌더링하지 않아도 문제가 없으므로 재렌더링되지 않도록 제어합니다.
React.memo
리액트에서 컴포넌트, 변수, 함수 등을 재렌러딩할 때 제어가 필요한 경우에는 메모이제이션(memoization)을 수행합니다. 메모이제이션은 이전 처리 결과를 저장해둠으로써 처리 속도를 높이는 기술입니다. 필요할 때만 다시 계산하게 하여 불필요한 처리를 줄일 수 있습니다.
이번 예에서는 컴포넌트를 메모이제이션해서 부모 컴포넌트가 재렌더링되더라도 자녀 컴포넌트의 재렌더링을 방지할 수 있습니다. 이 기능은 리액트가 제공하며 리액트 내의 memo를 사용합니다. 컴포넌트 함수 전체를 괄호로 감싸면 됩니다.
- memo
const Component = memo(() => {});
이렇게 컴포넌트를 괄호로 감싸면 해당 컴포넌트는 Props에 변경이 있을 때만 재렌더링됩니다.
그럼, 모든 컴포넌트를 메모이제이션해봅시다.
- 메모이제이션
App.jsx
import { useState, memo } from "react";
import { Child1 } from "./components/Child1";
import { Child4 } from "./components/Child4";;
export const App = memo(() => {
console.log("App 렌더링");
const [num, setNum] = useState(0);
const onClickButton = () => {
setNum(num+1);
};
return(
<>
<button onClick={onClickButton}>버튼</button>
<p>{num}</p>
<Child1 />
<Child4 />
</>
);
});
Child1.jsx
import { memo } from "react";
import { Child2 } from "./Child2";
import { Child3 } from "./Child3";
const style = {
height: "200px",
backgroundColor: "lightblue",
padding: "8px"
};
export const Child1 = memo(() => {
console.log("Child1 렌더링");
return (
<div style={style}>
<p>Child1</p>
<Child2 />
<Child3 />
</div>
);
});
Child2.jsx
import { memo } from "react";
const style = {
height: "50px",
backgroundColor: "Lightgray"
};
export const Child2 = memo(() => {
console.log("Child2 렌더링");
return(
<div style={style}>
<p>Child2</p>
</div>
);
});
Child3.jsx
import { memo } from "react";
const style = {
height: "50px",
backgroundColor: "lightgray"
};
export const Child3 = memo(() => {
console.log("Child3 렌더링");
return(
<div style={style}>
<p>Child3</p>
</div>
);
});
Child4.jsx
import { memo } from "react";
const style = {
height: "200px",
backgroundColor: "wheat",
padding: "8px"
};
export const Child4 = memo(() => {
console.log("Child4 렌더링");
return(
<div style={style}>
<p>Child4</p>
</div>
);
});
모든 컴포넌트를 메모이제이션했습니다. 이제 앞에서와 마찬가지로 카운트업을 실행해서 재렌더링한 결과를 확인해봅니다.


메모제이션 이후에는 App 컴포넌트만 재렌더링되는 것을 확인할 수 있습니다. 이렇게 memo를 사용함으로써 부모 컴포넌트의 재렌더링에 연결되어 불필요하게 재렌더링되는 것을 제어할 수 있습니다. 렌더링 비용이 높은 컴포넌트(요소 수가 많거나 부하가 높은 처리를 하는 등)는 적극적으로 메모이제이션을 활용함으로써 성능을 향상하면 됩니다.
렌더링 최적화2(useCallback)
memo를 사용해 컴포넌트를 메모이제이션할 수 있었습니다. 계속해서 함수 메모이제이션을 확인해봅니다.
사전 준비
먼저 Child1에 '클릭하면 카운트업 중인 수치를 0으로 되돌리는 [리셋] 버튼'을 배치하는 구현을 생각해봅니다. 수치의 State는 App이 가지고 있으므로 App안에서 리셋하기 위한 함수를 정의하고 그 함수를 Child1에 전달하는 방식으로 구현합니다.
- 사전 준비(App.jsx)
import { useState, memo } from "react";
import { Child1 } from "./components/Child1";
import { Child4 } from "./components/Child4";;
export const App = memo(() => {
console.log("App 렌더링");
const [num, setNum] = useState(0);
const onClickButton = () => {
setNum(num+1);
};
const onClickReset = () => {
setNum(0);
};
return(
<>
<button onClick={onClickButton}>버튼</button>
<p>{num}</p>
<Child1 onClickReset={onClickReset} />
<Child4 />
</>
);
});
- 사전 준비(Child1.jsx)
import { memo } from "react";
import { Child2 } from "./Child2";
import { Child3 } from "./Child3";
const style = {
height: "200px",
backgroundColor: "lightblue",
padding: "8px"
};
export const Child1 = memo((props) => {
console.log("Child1 렌더링");
//Props로 부터 함수를 전개(분할 대입)
const { onClickReset } = props;
return (
<div style={style}>
<p>Child1</p>
{/*전달된 함수를 실행하는 버튼 설정 */}
<button onClick={onClickReset}>리셋</button>
<Child2 />
<Child3 />
</div>
);
});
이로써 [리셋] 버튼을 클릭하면 수치가 초기화되는 기능을 만들었습니다.

그러나 앞에서 최적화했던 재렌더링을 확인해보면 카운트업을 할 때마다 Child1이 재렌더링되는 것을 알 수 있습니다.

마찬가지로 함수 정의도 Props가 변경되지 않았는데 재렌더링됩니다. 그러나 이는 원하는 결과가 아닙니다. 원인과 처리 방법을 확인해봅니다.
React.useCallback
함수를 Props에 전달할 때 컴포넌트를 메모이제이션해도 재렌더링되는 것은 함수가 다시 생성되기 때문입니다. 일반적으로 함수를 다음과 같이 정의합니다. 이렇게 함수를 정의하면 재렌더링 등으로 코드가 실행될 때마다 항상 새로운 함수가 다시 생성됩니다.
const onClickReset = () => {
setNum(0);
};
따라서 함수를 Props로 받는 Child1은 Props가 변화했다고 판정해 카운트업할 때마다 재렌더링을 하게 되는 것입니다. 이 현상을 피하기 위해서는 함수를 메모이제이션해야 합니다.
리액트는 함수 메모이제이션 기능 useCallback을 제공합니다. useCallback은 '첫 번째 인수에 함수' '두 번째 인수에 useEffect와 같은 의존 배열'을 받습니다. 다음은 그 예입니다.
- useCallback
const onClickButton = useCallback(() => {
alert('버튼을 클릭했습니다');
}, []);
이 경우 의존 배열은 비어 있으므로 함수는 처음 작성된 것을 재사용하게 됩니다. 물론 useEffect와 마찬가지로 의존 배열에 값을 설정했을 때는 그 값이 변경되는 시점에 다시 작성됩니다. 그럼 useCallback을 적용해봅니다.
- useCallback 적용(App.jsx)
import { useState, memo, useCallback } from "react";
import { Child1 } from "./components/Child1";
import { Child4 } from "./components/Child4";;
export const App = memo(() => {
console.log("App 렌더링");
const [num, setNum] = useState(0);
const onClickButton = () => {
setNum(num+1);
};
const onClickReset = useCallback(() => {
setNum(0);
}, []);
return(
<>
<button onClick={onClickButton}>버튼</button>
<p>{num}</p>
<Child1 onClickReset={onClickReset} />
<Child4 />
</>
);
});
onClickReset 함수를 useCallback을 사용해 메모이제이션했을 뿐입니다. 이 상태에서 카운트업 처리를 실행하면 카운트업할 때 App만 재렌더링되는 것을 확인할 수 있습니다.

App만 재렌더링되어 불필요한 재렌더링을 최적화했습니다. 이렇게 자녀 컴포넌트에 Props로 전달하는 함수는 적극적으로 useCallback을 사용해 메모이제이션함으로써 의도하지 않은 재렌더링이 발생하지 않도록 합니다.
변수 메모제이션(useMemo)
지금까지 컴포넌트 메모이제이션과 함수 메모이제이션에 관해 설명했습니다. 기본적으로는 이 두가지를 사용하면 불필요한 재렌더링을 제어할 수 있을 것입니다. 마지막으로 변수 메모이제이션을 소개합니다.
React.useMemo
memo나 useCallback만큼 자주 사용되지는 않지만 리액트에서는 변수 메모이제이션으로 use-Memo를 제공합니다. useMemo의 구문은 다음과 같습니다.
- useMemo 구문
const sum = useMemo(() => {
return 1 + 3;
}, []);
useEffect나 useCallback과 구문은 거의 같습니다. '첫 번째 인수의 함수에 변수에 설정할 값의 반환' '두 번째 인수에 의존 배열'을 전달합니다.
구문을 기준으로 설명하면 두 번째 인수가 빈 배열이므로 최초 로딩 되었을 때만 '1 + 3'이라는 계산을 실행하고, 그 이후 재렌더링될 때는 최초의 값을 다시 사용할 수 있게 됩니다. 물론 기존 배열에 변수를 설정해두면 그 값이 변했을 때만 변수를 재설정할 수 있습니다.
변수 정의 로직이 복잡하거나 많은 수의 루프가 실행되는 경우 등에 사용함으로써 변수 설정에 의한 부하를 낮출 수 있습니다. 그리고 의존 배열에 설정된 값을 참조함으로써 변수를 설정하는 데 영향을 주는 외부값을 명시적으로 나타낼 수 있어 가독성 향상을 기대할 수 있습니다.
'리액트' 카테고리의 다른 글
Link vs useNavigate() (0) | 2025.03.04 |
---|---|
리액트와 CSS (0) | 2025.02.13 |
리액트 기본 (2) | 2025.02.03 |