React의 리렌더링은 왜 일어날까: 기본 동작 방식부터 다시 이해하기
React를 공부하다 보면 리렌더링이라는 말을 정말 자주 듣게 됩니다.
- state가 바뀌면 리렌더링된다
- 부모가 리렌더링되면 자식도 리렌더링된다
React.memo로 막을 수 있다- 불필요한 리렌더링을 줄여야 한다
그런데 막상 여기서 한 번 멈칫하게 됩니다.
- 리렌더링은 정확히 무엇이 다시 일어난다는 뜻이지?
- 컴포넌트 함수가 다시 실행된다는 말과 DOM이 바뀐다는 말은 같은 뜻일까?
- 부모가 렌더링되면 자식도 무조건 다시 그려지는 걸까?
- 왜 어떤 리렌더링은 괜찮고, 어떤 경우만 줄이면 되는 걸까?
이 질문을 제대로 잡지 않으면 실무에서 성능 최적화를 할 때도 자꾸 감각적으로만 접근하게 됩니다.
- 무조건
memo를 붙이고 - 무조건
useCallback을 쓰고 - 무조건 "리렌더링 = 나쁜 것"처럼 받아들이기 쉽습니다
하지만 리렌더링은 최적화 대상이기 전에, 먼저 이해해야 할 React의 기본 동작입니다.
이 글에서는 아래 흐름으로 정리해보겠습니다.
- 리렌더링이 정확히 무엇인지
- 컴포넌트 함수가 다시 실행된다는 말의 의미
- state, props, parent render, context가 어떻게 연결되는지
- 리렌더링과 실제 DOM 변경이 왜 같은 말이 아닌지
- reconciliation, commit, key는 어떤 역할을 하는지
- 왜 어떤 리렌더링만 줄이면 되는지
한눈에 보면
먼저 짧게 정리하면 이렇습니다.
React에서 리렌더링은 보통 컴포넌트 함수가 다시 실행되어 새로운 UI 결과를 계산하는 과정에 가깝습니다.- 리렌더링이 일어났다고 해서 실제 DOM이 항상 바뀌는 것은 아닙니다.
- state, props, context, 부모 렌더링은 리렌더링의 계기가 될 수 있습니다.
React는 이전 결과와 다음 결과를 비교한 뒤, 실제로 바뀐 부분만 DOM에 반영하려고 합니다.
즉, 리렌더링은 곧바로 "화면을 전부 다시 그린다"는 뜻이 아니라, 무엇이 바뀌었는지 다시 계산하고 필요한 부분만 반영하려는 과정으로 이해하는 편이 더 정확합니다.
리렌더링은 정확히 무엇일까?
처음에는 "리렌더링 = 다시 그리기"라고 이해하기 쉽습니다. 물론 직관적으로는 나쁘지 않습니다. 하지만 기본기 관점에서는 조금 더 정확히 볼 필요가 있습니다.
React에서 리렌더링은 보통 아래 흐름으로 생각할 수 있습니다.
- 어떤 변화가 생긴다
- 컴포넌트 함수가 다시 실행된다
- 새로운 JSX 결과가 계산된다
- 이전 결과와 비교한다
- 실제로 바뀐 부분만 반영한다
즉, 리렌더링은 단순히 화면 픽셀을 무조건 다시 칠하는 일이 아니라, 변화 이후의 UI가 어떤 모습이어야 하는지 다시 계산하는 과정에 더 가깝습니다.
컴포넌트 함수가 다시 실행된다는 건 무슨 뜻일까?
가장 먼저 이 감각이 필요합니다.
function Counter() {
const [count, setCount] = useState(0);
console.log('render');
return <button onClick={() => setCount((prev) => prev + 1)}>{count}</button>;
}여기서 버튼을 누르면 setCount가 호출되고, Counter 함수는 다시 실행됩니다.
이때 중요한 건:
- 함수가 다시 실행된다고 해서 컴포넌트 인스턴스가 완전히 새로 만들어지는 것과는 조금 다르고
- 기존 상태를 바탕으로 새로운 렌더 결과를 다시 계산한다고 보는 편이 맞다는 점입니다
즉, React 컴포넌트는 단순한 템플릿이 아니라 현재 state와 props를 입력으로 받아 UI 결과를 계산하는 함수처럼 이해할 수 있습니다.
그래서 입력값이 바뀌면 다시 실행되는 것입니다.
state가 바뀌면 왜 리렌더링될까?
이건 비교적 직관적입니다.
function Profile() {
const [name, setName] = useState('Marco');
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>{name}</p>
</>
);
}name이 바뀌면 화면에 보여줘야 할 값도 바뀝니다.
즉, state는 단순한 변수라기보다 UI 계산에 참여하는 값입니다. 이 값이 바뀌면 React는 "그럼 지금 UI 결과도 달라졌을 수 있겠네"라고 보고 다시 계산합니다.
즉, state 변경 후 리렌더링은 최적화 실패가 아니라 아주 정상적인 기본 동작입니다.
props가 바뀌면 왜 리렌더링될까?
이것도 같은 맥락입니다.
function Parent() {
const [count, setCount] = useState(0);
return <Child count={count} />;
}
function Child({ count }: { count: number }) {
return <p>{count}</p>;
}Parent의 state가 바뀌면 Child에 내려가는 count도 달라질 수 있습니다.
즉, props는 자식 컴포넌트 입장에서 외부 입력값입니다. 입력값이 바뀌면 결과도 달라질 수 있으니 다시 계산하는 것이 자연스럽습니다.
부모가 리렌더링되면 자식도 왜 다시 실행될 수 있을까?
여기서 많이 헷갈립니다.
많은 사람이 "자식 props가 안 바뀌었는데 왜 자식도 다시 렌더링되지?"라고 느낍니다.
예를 들어:
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((prev) => prev + 1)}>+</button>
<Child />
</>
);
}
function Child() {
console.log('child render');
return <div>child</div>;
}이 경우 부모가 다시 렌더링되면 자식 함수도 다시 실행될 수 있습니다.
왜냐하면 부모의 렌더 결과를 다시 계산하는 과정에서, 그 안에 포함된 자식도 함께 다시 평가되는 흐름이 기본이기 때문입니다.
즉, 기본적으로 React는 "부모가 다시 렌더링됐다면 자식도 다시 확인해보자"에 가깝게 동작합니다. 다만 이 확인이 곧 실제 DOM 변경을 의미하는 것은 아닙니다.
context가 바뀌면 왜 연결된 컴포넌트가 다시 렌더링될까?
context도 결국 입력값입니다.
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return <div>{theme}</div>;
}Toolbar는 직접 props를 받지 않더라도, ThemeContext 값을 읽고 있습니다.
즉, context를 읽는 컴포넌트 입장에서는 context도 props처럼 렌더링 결과에 영향을 주는 외부 입력입니다. 그래서 context 값이 바뀌면 다시 렌더링되는 것이 자연스럽습니다.
리렌더링과 실제 DOM 변경은 같은 말일까?
아닙니다. 이 부분이 정말 중요합니다.
많은 사람이 리렌더링을 "브라우저 화면을 다시 다 갈아엎는 것"처럼 느끼는데, 실제로는 그렇지 않습니다.
예를 들어:
function StaticChild() {
console.log('render static child');
return <div>Hello</div>;
}이 컴포넌트 함수가 다시 실행되더라도, 이전 결과와 다음 결과가 같다면 실제 DOM은 크게 바뀌지 않을 수 있습니다.
즉:
- 컴포넌트 함수 재실행
- 가상 결과 비교
- 실제 DOM 반영
은 같은 단계가 아닙니다.
리렌더링은 주로 앞단의 계산 과정이고, 실제 DOM 변경은 그 결과로 필요한 부분만 일어나는 마지막 반영 단계에 가깝습니다.
React는 어떻게 필요한 부분만 반영할까?
여기서 나오는 개념이 보통 reconciliation입니다.
아주 단순하게 말하면 React는 이전 렌더 결과와 다음 렌더 결과를 비교해서, 무엇이 달라졌는지 판단하려고 합니다.
예를 들어:
function App({ isLoggedIn }: { isLoggedIn: boolean }) {
return (
<div>
<h1>My App</h1>
{isLoggedIn ? <p>Welcome</p> : <p>Please login</p>}
</div>
);
}여기서 h1은 그대로이고 p의 내용만 달라질 수 있습니다.
그렇다면 React는 가능하면 바뀐 부분만 반영하려고 합니다.
즉, React의 핵심은 "무조건 다시 그림"이 아니라, 새로운 결과를 계산한 뒤 차이를 비교해서 필요한 부분만 갱신하려는 것입니다.
render 단계와 commit 단계는 어떻게 다를까?
실무에서 이 구분도 도움이 됩니다.
아주 단순하게 나누면:
- render 단계: 어떤 UI가 되어야 하는지 계산하는 단계
- commit 단계: 실제 DOM에 반영하는 단계
즉, 어떤 컴포넌트가 다시 렌더링됐다는 말은 보통 render 단계에서 다시 계산됐다는 뜻에 더 가깝습니다.
반면 화면이 실제로 바뀌는 것은 commit 단계와 더 연결됩니다.
이 구분을 이해하면 "렌더링이 있었다"와 "실제 화면이 많이 바뀌었다"를 섞어 생각하는 일이 줄어듭니다.
key는 여기서 왜 중요할까?
리스트 렌더링에서 key가 중요한 이유도 결국 비교 기준과 연결됩니다.
{
todos.map((todo) => <TodoItem key={todo.id} todo={todo} />);
}key는 React가 리스트 안의 각 항목을 식별하는 기준이 됩니다.
만약 key가 불안정하면:
- 어떤 항목이 그대로 있는지
- 어떤 항목이 새로 생겼는지
- 어떤 항목이 사라졌는지
를 정확히 판단하기 어려워집니다.
즉, key는 단순 경고를 없애기 위한 문법이 아니라, 렌더 결과를 비교할 때 identity를 유지하는 힌트에 가깝습니다.
왜 어떤 리렌더링은 괜찮고, 어떤 경우만 줄이면 될까?
이제 조금 더 감이 옵니다.
리렌더링 자체는 React의 정상 메커니즘입니다. 아주 가벼운 컴포넌트라면 몇 번 더 다시 실행돼도 거의 문제가 되지 않을 수 있습니다.
실제로 줄일 가치가 큰 경우는 보통 아래처럼 조금 더 구체적입니다.
- 리스트 항목 수가 많을 때
- 계산 비용이 큰 컴포넌트일 때
- 입력 한 번에 큰 하위 트리가 계속 다시 계산될 때
- context 변화가 너무 넓은 범위에 퍼질 때
즉, 핵심은 "리렌더링이 일어났다"가 아니라, 실제 변화와 관계없는 계산이 너무 넓게 반복되고 있는가를 보는 것입니다.
React.memo는 무엇을 막는 걸까?
이 개념도 여기서 더 잘 이해됩니다.
React.memo는 보통 props가 같다면 자식 컴포넌트의 재실행을 건너뛰도록 도와줍니다.
즉, memo는 리렌더링 전체를 없애는 기능이라기보다, 부모 렌더링에 따라 습관적으로 다시 실행되던 자식을 조건부로 건너뛰는 도구에 가깝습니다.
그래서 memo가 효과를 보려면:
- 정말 자주 다시 실행되는 자식이 있고
- props가 자주 그대로 유지되며
- 그 자식이 가볍지 않은 경우
를 함께 봐야 합니다.
useCallback, useMemo는 왜 같이 나오게 될까?
이것도 결국 props 비교와 연결됩니다.
예를 들어 부모가 렌더링될 때마다 새 함수나 새 객체를 만들면, 자식 입장에서는 props가 매번 달라진 것처럼 보일 수 있습니다.
그래서:
useCallback은 함수 참조를 안정화하고useMemo는 계산 결과나 객체 참조를 안정화하는 데 쓰일 수 있습니다
즉, 이 훅들은 "성능 최적화용 마법"이 아니라, 리렌더링 비교 기준을 안정적으로 유지하는 보조 도구에 가깝습니다.
Strict Mode에서는 왜 더 헷갈릴까?
개발 중에는 "왜 렌더링이 두 번 일어나지?"라는 의문이 생길 수 있습니다.
이건 특히 StrictMode에서 더 자주 느껴집니다.
개발 환경에서 StrictMode는 일부 동작을 더 엄격하게 확인하기 위해 렌더 관련 함수를 추가로 실행해보기도 합니다.
즉, 개발 중 콘솔에서 렌더 로그가 예상보다 더 많이 찍힌다고 해서 곧바로 프로덕션에서도 똑같이 문제가 생긴다고 단정하면 안 됩니다.
물론 그렇다고 무시하자는 뜻은 아닙니다. 다만 개발 모드의 특성과 실제 프로덕션 렌더링을 구분해서 보는 감각이 필요합니다.
리렌더링을 이해할 때 자주 하는 오해
정리하면 아래 오해가 정말 많습니다.
1. 리렌더링 = DOM 전체 다시 그리기
아닙니다. 보통은 먼저 함수 실행과 결과 비교가 일어나고, 실제 DOM 반영은 필요한 부분만 일어납니다.
2. 부모가 렌더링돼도 자식은 props가 같으면 절대 안 그려진다
기본적으로는 부모 렌더 과정에서 자식도 다시 실행될 수 있습니다. 이를 건너뛰는 쪽이 memo 같은 최적화입니다.
3. 리렌더링은 무조건 나쁘다
아닙니다. 가벼운 컴포넌트라면 자연스럽게 다시 렌더링되는 편이 더 단순할 수 있습니다.
4. memo, useCallback, useMemo만 알면 해결된다
아닙니다. 실제로는 state 위치, props 설계, context 범위가 더 먼저인 경우가 많습니다.
실무에서는 이 이해가 왜 중요할까?
이 감각이 없으면:
- 리렌더링이 보이면 무조건 훅부터 붙이게 되고
- 왜 느린지보다 "막아야 한다"에만 집중하게 되고
- 최적화 전후 차이를 설명하기 어려워집니다
반대로 이 감각이 있으면:
- 어떤 변화가 어떤 범위까지 전파되는지 보고
- 어떤 자식이 굳이 다시 계산될 필요가 없는지 판단하고
memo,useCallback,useMemo를 어디에 써야 할지 더 명확해집니다
즉, 리렌더링 최적화의 출발점은 훅이 아니라 기본 렌더링 모델에 대한 이해입니다.
같이 보면 좋은 글
- React의 리렌더링은 왜 일어나고 실무에서는 어떻게 줄일까
- controlled vs uncontrolled component는 무엇이고 언제 무엇을 선택할까
- Compound Component 패턴은 무엇이고 React에서 어떻게 적용할까
- Render Props vs Custom Hooks vs HOC, 언제 무엇을 선택할까
결론
React의 리렌더링은 막아야 할 이상 현상이 아니라, UI를 최신 상태로 맞추기 위한 기본 동작입니다.
짧게 정리하면:
- 리렌더링은 보통 컴포넌트 함수가 다시 실행되어 새 결과를 계산하는 과정이고
- 그것이 곧 실제 DOM 전체 변경을 뜻하는 것은 아니며
React는 이전 결과와 비교해서 필요한 부분만 반영하려고 합니다
결국 리렌더링을 잘 이해한다는 것은 "다시 그리지 않게 만드는 법"을 아는 것보다 먼저, 무엇이 왜 다시 계산되고 무엇이 실제로 바뀌는가를 구분해서 보는 감각을 가지는 일에 더 가깝습니다.
