"버튼 하나 눌렀을 뿐인데 왜 전체가 버벅이지?"
리액트는 부모 컴포넌트가 재렌더링되면 자식 컴포넌트도 무조건 함께 재렌더링됩니다. 평소엔 별문제가 없지만, 자식 컴포넌트가 무거운 연산을 하거나 렌더링 시간이 오래 걸리는 경우엔 심각한 성능 저하로 이어집니다. 이걸 해결하는 도구가 바로 memo와 useMemo입니다.
📌 먼저 알아야 할 것 — 리액트의 재렌더링 규칙
리액트는 아래 상황에서 컴포넌트를 재렌더링합니다.
- 해당 컴포넌트의 state가 변경될 때
- 부모로부터 받은 props가 변경될 때
- 부모 컴포넌트가 재렌더링될 때 (자식도 무조건 같이 재렌더링)
세 번째 규칙이 문제입니다. 부모가 재렌더링되면 자식과 전혀 관계없는 이유라도 자식이 덩달아 재렌더링됩니다.
function Child() {
console.log("재렌더링됨") // 부모 버튼 누를 때마다 이게 찍힘
return <div>자식임</div>
}
function Cart() {
let [count, setCount] = useState(0);
return (
<div>
<Child />
<button onClick={() => { setCount(count + 1) }}>+</button>
</div>
)
}
+ 버튼을 누르면 Cart의 count state가 바뀌어 Cart가 재렌더링됩니다. 그런데 Child는 count와 전혀 관계가 없는데도 함께 재렌더링됩니다. Child가 무거운 컴포넌트라면 버튼 누를 때마다 버벅이는 현상이 생기는 것입니다.
✅ 해결책 1 — memo : 불필요한 자식 재렌더링 막기
memo로 컴포넌트를 감싸면 "props가 바뀔 때만 재렌더링해주세요" 라는 의미가 됩니다.
import { memo, useState } from 'react'
// ✅ memo로 감싸기
let Child = memo(function() {
console.log("재렌더링됨")
return <div>자식임</div>
})
function Cart() {
let [count, setCount] = useState(0);
return (
<div>
<Child />
<button onClick={() => { setCount(count + 1) }}>+</button>
</div>
)
}
이제 + 버튼을 눌러도 Child로 전달되는 props가 없으니 Child는 재렌더링되지 않습니다.
만약 props를 전달하는 경우라면, 해당 props가 실제로 바뀔 때만 재렌더링됩니다.
// count가 바뀔 때만 Child 재렌더링됨
<Child count={count} />
⚠️ memo를 남발하면 안 되는 이유
memo로 감싼 컴포넌트는 재렌더링을 막으려고 매번 "기존 props == 새 props" 를 비교하는 연산을 추가로 수행합니다.
- props가 단순한 숫자나 문자열이면 비교 비용이 거의 없음
- props가 크고 복잡한 객체/배열이면 비교 자체가 오히려 성능 손해가 될 수 있음
따라서 memo는 렌더링 비용이 크고 props 변화가 적은 컴포넌트에만 선택적으로 사용하는 게 좋습니다.
✅ 해결책 2 — useMemo : 무거운 연산 결과를 기억해두기
memo는 컴포넌트 자체의 재렌더링을 막는 것이고, useMemo는 컴포넌트 안의 특정 연산 결과를 기억(메모이제이션)해두는 것입니다.
문제 상황
function 함수() {
return 반복문을_10억번_돌린_결과
}
function Cart() {
let result = 함수() // ❌ 재렌더링될 때마다 매번 10억 번 반복문 실행!
return ( ... )
}
Cart가 재렌더링될 때마다 함수()가 매번 새로 실행됩니다. 연산이 무거울수록 심각한 성능 문제가 됩니다.
useMemo로 해결
import { useMemo, useState } from 'react'
function 함수() {
return 반복문을_10억번_돌린_결과
}
function Cart() {
// ✅ 처음 1회만 실행하고 결과를 기억해둠
let result = useMemo(() => { return 함수() }, [])
return ( ... )
}
useMemo의 구조는 useEffect와 비슷합니다.
useMemo(() => { 실행할 함수 }, [의존성 배열])
- 첫 번째 인자: 기억해둘 연산을 담은 함수. 반드시 값을 return해야 합니다.
- 두 번째 인자(의존성 배열): useEffect처럼 특정 state/props가 바뀔 때만 재실행하도록 설정. 빈 배열 []이면 컴포넌트 최초 로드 시 딱 1회만 실행됩니다.
// state가 바뀔 때만 함수 재실행
let result = useMemo(() => { return 함수() }, [state])
// 컴포넌트 최초 로드 시 1회만 실행
let result = useMemo(() => { return 함수() }, [])
🔍 memo vs useMemo vs useEffect 비교
세 가지가 비슷해 보여서 헷갈리기 쉽습니다. 아래 표로 차이를 정리하면 이렇습니다.
| memo | 불필요한 재렌더링 방지 | 컴포넌트 전체 | props가 바뀔 때만 렌더링 |
| useMemo | 무거운 연산 결과 재사용 | 컴포넌트 내 특정 값 | 렌더링과 동시에 실행 |
| useEffect | 렌더링 후 사이드이펙트 처리 | 렌더링 후 동작 | return(HTML) 실행이 끝난 뒤 실행 |
💡 메모의 핵심 차이: useEffect는 화면이 그려진 이후에 실행되고, useMemo는 화면이 그려지는 과정에 함께 실행됩니다. 따라서 렌더링 결과물에 영향을 주는 연산이라면 useMemo를 써야 합니다.
🛒 실전 예시 — 장바구니 컴포넌트에 적용하기
메모에 있던 코드를 기반으로 실제 장바구니 컴포넌트에서 함께 쓰이는 모습을 살펴봅시다.
import { memo, useMemo, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increase } from './store/cartSlice'
// ✅ memo로 감싸서 불필요한 재렌더링 방지
let Child = memo(function({ count }) {
console.log("재렌더링됨")
return <div>자식 컴포넌트 (count: {count})</div>
})
// 무거운 연산 함수
function 무거운함수() {
// 반복문 10억 번 같은 무거운 연산
return 결과값
}
function Cart() {
// ✅ useMemo로 무거운 연산 결과 캐싱
let result = useMemo(() => { return 무거운함수() }, [])
// Redux에서 전체 state 가져오기
let state = useSelector((state) => { return state })
// Redux에서 cart 배열만 가져오기
let cart = useSelector((state) => { return state.cart })
// dispatch: Redux store에 액션을 보내는 함수
let dispatch = useDispatch()
let [count, setCount] = useState(0)
return (
<div>
{/* count가 바뀔 때만 Child 재렌더링 */}
<Child count={count} />
<button onClick={() => { setCount(count + 1) }}>+</button>
{state.user.name} {state.user.age}의 장바구니
<button onClick={() => {
dispatch(increase(100))
}}>수량 증가</button>
<table>
<thead>
<tr>
<th>#</th><th>상품명</th><th>수량</th><th>변경하기</th>
</tr>
</thead>
<tbody>
{cart.map((element) => (
<tr key={element.id}>
<td>{element.id}</td>
<td>{element.name}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
🔖 정리
| 부모 재렌더링 시 자식이 불필요하게 같이 렌더링됨 | memo로 자식 컴포넌트 감싸기 |
| 렌더링마다 무거운 연산이 반복 실행됨 | useMemo로 연산 결과 캐싱 |
| props가 크고 복잡해서 memo 비교 자체가 부담스러움 | memo 사용 자제, 꼭 필요한 곳에만 사용 |
| 렌더링 후에 뭔가 실행시키고 싶음 | useMemo 대신 useEffect 사용 |
성능 최적화는 무조건 처음부터 모든 곳에 적용하는 게 아니라, Profiler로 실제 느린 곳을 찾은 뒤 필요한 곳에만 적용하는 것이 올바른 순서입니다!
'공부 > frontend' 카테고리의 다른 글
| React Hook Form으로 로그인 폼 만들기 (입력 검증 + 접근성까지) (0) | 2026.07.02 |
|---|---|
| setState를 썼는데 값이 왜 이상하지? — async 완전 이해하기 (0) | 2026.07.01 |
| React 성능 개선1 — 개발자도구 활용 & lazy import (0) | 2026.07.01 |
| Tanstack Query(React Query) 완전 정복 — 실시간 데이터를 쉽게 다루는 법 (0) | 2026.07.01 |
| 리액트에서 자주 쓰는 if문 작성 패턴 5가지 (0) | 2026.06.17 |