이 글에서 정리하는 내용
React에서 Cannot update a component while rendering a different component 경고가 발생했을 때, 단순히 setState를 없애는 방식이 아니라 호출 위치를 기준으로 원인을 찾는 방법을 정리합니다. 자식 컴포넌트가 부모 상태를 렌더링 중 바꾸는 상황, 이벤트 핸들러로 옮겨야 하는 경우, 초기값 설계로 풀어야 하는 경우, useEffect를 써도 되는 경우를 나누어 봅니다.
- 경고가 뜨는 상황부터 확인하기
- 렌더링 중 setState가 문제가 되는 이유
- 다른 컴포넌트를 업데이트한다는 말의 의미
- 이벤트 핸들러로 옮겨야 하는 경우
- 초기값은 렌더링 중 보정하지 않기
- useEffect로 옮겨도 되는 경우와 조심할 점
- 다시 발생하지 않게 확인할 체크포인트
경고가 뜨는 상황부터 확인하기

React 작업 중 화면은 일단 보이는데 콘솔에 긴 경고가 찍히는 경우가 있습니다. 그중 Cannot update a component while rendering a different component는 상태 변경 위치가 렌더링 단계와 맞지 않을 때 자주 나옵니다.
이 경고는 setState를 사용했다는 사실만으로 발생하지 않습니다. 버튼 클릭, 입력 변경, 비동기 요청 완료처럼 렌더링 바깥에서 상태를 바꾸는 것은 React에서 흔한 작업입니다. 문제는 컴포넌트가 JSX를 계산하는 도중에 다른 컴포넌트의 상태를 바꾸려고 할 때 생깁니다.
예를 들어 상품 카드 목록에서 첫 번째 상품을 자동 선택하려고 할 때를 생각할 수 있습니다. 부모 컴포넌트는 selectedId를 가지고 있고, 자식 카드 컴포넌트는 상품 정보를 받아 화면에 그립니다. 그런데 자식 컴포넌트가 렌더링되는 본문에서 “선택된 값이 없으면 내 상품을 선택하자”라고 판단해 부모의 setSelectedId를 바로 호출하면 경고가 발생할 수 있습니다.
문제가 되는 코드
import { useState } from 'react';
function ProductList({ products }) {
const [selectedId, setSelectedId] = useState(null);
return (
<ul>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
selectedId={selectedId}
setSelectedId={setSelectedId}
/>
))}
</ul>
);
}
function ProductCard({ product, selectedId, setSelectedId }) {
if (selectedId === null) {
setSelectedId(product.id);
}
return (
<li>
<button>{product.name}</button>
</li>
);
}
겉으로 보면 조건문이 있으니 한 번만 실행될 것처럼 보입니다. 하지만 React 입장에서는 ProductCard를 렌더링하는 중에 ProductList의 상태 변경 요청이 들어온 상태입니다. 현재 화면을 계산하는 도중 부모 컴포넌트를 다시 업데이트해야 하므로 렌더링 순서가 불안정해집니다.
렌더링 중 setState가 문제가 되는 이유
React 컴포넌트의 본문은 화면을 계산하는 구간입니다. 이 구간에서는 props, state, context를 읽고 이번 렌더에서 어떤 JSX를 반환할지 결정합니다. 같은 입력이 들어오면 같은 결과를 계산할 수 있어야 렌더링 순서를 React가 안정적으로 관리할 수 있습니다.
그런데 렌더링 본문에서 상태를 바꾸면 상황이 달라집니다. 아직 현재 렌더가 끝나지 않았는데 다시 렌더해야 하는 일이 생깁니다. 같은 컴포넌트 안에서 무조건 상태를 바꾸면 무한 렌더링으로 이어질 수 있고, 다른 컴포넌트의 상태를 바꾸면 이번 글의 경고처럼 “다른 컴포넌트를 렌더링 중 업데이트했다”는 메시지가 나올 수 있습니다.
처음에는 이 차이가 잘 보이지 않습니다. 코드상으로는 setSelectedId 한 줄뿐이고, 조건문 안에 있으니 안전해 보입니다. 하지만 기준은 조건문의 유무가 아니라 그 호출이 어느 단계에서 실행되는지입니다. 컴포넌트 함수 본문에서 실행되면 렌더링 중 실행되는 코드입니다.
| 상태 변경 위치 | 판단 |
|---|---|
| 버튼 클릭 이벤트 안 | 사용자 행동 이후 실행되므로 일반적으로 적절합니다. |
| 입력 변경 이벤트 안 | 폼 값 변경처럼 명확한 원인이 있어 자연스럽습니다. |
| 컴포넌트 함수 본문 | 렌더링 중 실행되므로 경고나 무한 렌더링 원인이 될 수 있습니다. |
useEffect 안 | 렌더 후 동기화가 필요할 때만 검토합니다. 무조건 옮기는 해결책은 아닙니다. |
초기 useState 계산 함수 | 처음 state 값을 정하는 목적이면 적절한 선택이 될 수 있습니다. |
다른 컴포넌트를 업데이트한다는 말의 의미
경고 문구의 핵심은 different component입니다. 예시에서는 ProductCard가 렌더링되고 있는데, 그 안에서 ProductList가 가진 상태를 바꾸고 있습니다. 자식 컴포넌트가 부모의 setter를 props로 받는 구조 자체가 문제는 아닙니다. 문제는 그 setter를 렌더링 본문에서 바로 호출한다는 점입니다.
부모가 자식에게 setSelectedId를 넘기는 패턴은 실제 화면에서 자주 씁니다. 탭 메뉴, 필터 버튼, 상품 카드, 관리자 테이블의 행 선택 같은 UI에서 자식이 클릭되면 부모 state를 바꿔야 합니다. 이때는 자식이 부모 상태를 바꾼다는 구조보다, 그 호출이 사용자 이벤트 이후에 실행되는지가 더 중요합니다.
따라서 이 경고를 보면 먼저 검색해야 할 위치는 return 위쪽입니다. 컴포넌트 본문에 setSomething, dispatch, navigate, 전역 store 업데이트 함수가 조건문과 함께 놓여 있는지 확인합니다. 상태 변경 함수 이름이 직접 보이지 않아도, 내부에서 상태를 바꾸는 헬퍼 함수를 호출하고 있을 수 있습니다.
이벤트 핸들러로 옮겨야 하는 경우
사용자 행동 때문에 상태가 바뀌는 값이라면 이벤트 핸들러로 옮기는 것이 가장 먼저 볼 수정 방향입니다. 상품을 선택한다는 동작은 사용자가 카드를 클릭했을 때 발생하는 일이므로, 렌더링 중 자동으로 처리할 필요가 없습니다.
수정 코드
import { useState } from 'react';
function ProductList({ products }) {
const [selectedId, setSelectedId] = useState(null);
return (
<ul>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
isSelected={selectedId === product.id}
onSelect={() => setSelectedId(product.id)}
/>
))}
</ul>
);
}
function ProductCard({ product, isSelected, onSelect }) {
return (
<li>
<button type="button" onClick={onSelect} aria-pressed={isSelected}>
{product.name}
</button>
</li>
);
}
이 코드에서는 ProductCard가 렌더링되는 동안 부모 상태를 바꾸지 않습니다. 버튼이 클릭된 뒤 onSelect가 실행되고, 그때 부모의 setSelectedId가 호출됩니다. 같은 setter를 자식에게 넘겨도 호출 타이밍이 달라지면 경고가 사라집니다.
여기서 이름도 같이 정리해두면 좋습니다. 자식에게 setSelectedId를 그대로 넘기기보다 onSelect처럼 이벤트 의미가 드러나는 이름으로 넘기면, 자식 입장에서는 “부모 state를 직접 조작한다”보다 “선택 이벤트를 부모에게 알린다”는 구조가 됩니다.
초기값은 렌더링 중 보정하지 않기
경고가 나는 코드 중에는 사용자 이벤트가 아니라 초기값을 맞추려다 생긴 경우도 많습니다. “목록이 있으면 첫 번째 항목을 기본 선택해야 한다”는 요구사항이 대표적입니다. 이때 자식 컴포넌트가 렌더링 중 부모 state를 보정하게 만들면 구조가 꼬입니다.
초기값이 이미 렌더링 전에 정해질 수 있다면 부모에서 state를 만들 때 처리하는 쪽이 더 단순합니다. 목록이 정적인 값이거나, 컴포넌트가 처음 렌더링될 때 이미 들어와 있는 값이라면 useState 초기 계산 함수를 사용할 수 있습니다.
부모에서 초기 선택값을 정하는 코드
import { useState } from 'react';
function ProductList({ products }) {
const [selectedId, setSelectedId] = useState(() => {
return products[0]?.id ?? null;
});
return (
<ul>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
isSelected={selectedId === product.id}
onSelect={() => setSelectedId(product.id)}
/>
))}
</ul>
);
}
이 방식은 “처음부터 어떤 값으로 시작할 것인가”를 부모가 책임집니다. 자식 컴포넌트는 자신을 그리는 일과 클릭 이벤트를 전달하는 일만 맡습니다. 상태의 출처가 한 곳에 모이면 나중에 필터, 정렬, 페이지네이션이 붙어도 문제를 추적하기가 훨씬 쉽습니다.
다만 서버 요청이나 비동기 데이터처럼 처음 렌더링 시점에는 products가 비어 있고 나중에 들어오는 경우도 있습니다. 그때는 초기 계산 함수만으로 해결되지 않을 수 있습니다. 이 경우에도 자식 렌더링 본문에서 보정하기보다, 데이터를 받는 부모 쪽에서 조건을 명확하게 잡아 처리해야 합니다.
useEffect로 옮겨도 되는 경우와 조심할 점
useEffect는 렌더링이 끝난 뒤 실행됩니다. 그래서 렌더링 본문에서 바로 호출하던 상태 변경을 useEffect 안으로 옮기면 경고가 사라지는 경우가 있습니다. 하지만 모든 문제를 useEffect로 보내면 다른 형태의 불필요한 렌더링이나 의존성 루프가 생길 수 있습니다.
useEffect가 어울리는 경우는 렌더링 결과와 별개로 외부 값이 들어온 뒤 상태를 맞춰야 할 때입니다. 예를 들어 서버에서 상품 목록을 받은 뒤, 선택값이 비어 있을 때만 첫 번째 항목을 선택해야 한다면 부모 컴포넌트에서 조건을 걸어 처리할 수 있습니다.
비동기 데이터 이후 선택값을 맞추는 코드
import { useEffect, useState } from 'react';
function ProductList({ products }) {
const [selectedId, setSelectedId] = useState(null);
useEffect(() => {
if (selectedId !== null) return;
if (products.length === 0) return;
setSelectedId(products[0].id);
}, [products, selectedId]);
return (
<ul>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
isSelected={selectedId === product.id}
onSelect={() => setSelectedId(product.id)}
/>
))}
</ul>
);
}
이 코드는 렌더링 본문에서 바로 상태를 바꾸지 않습니다. 렌더링이 끝난 뒤 조건을 확인하고, 선택값이 아직 없고 목록이 있을 때만 상태를 바꿉니다. 조건문이 없으면 products나 selectedId 변화에 따라 다시 상태 변경이 반복될 수 있으므로, effect 안에서는 종료 조건을 먼저 잡아야 합니다.
반대로 단순히 props를 가공해서 보여주는 정도라면 effect와 state를 추가하지 않는 편이 낫습니다. 예를 들어 상품 목록을 정렬하거나 필터링한 결과를 화면에 보여주는 일은 렌더링 중 계산해도 됩니다. 상태 변경이 아니라 값 계산이기 때문입니다.
state로 옮기지 않아도 되는 계산
function ProductList({ products, keyword }) {
const filteredProducts = products.filter((product) => {
return product.name.includes(keyword);
});
return (
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
이 코드는 렌더링 중 실행되지만 문제가 되지 않습니다. filteredProducts는 새로운 상태를 만들지 않고, 현재 props를 기준으로 이번 렌더에서 보여줄 값을 계산할 뿐입니다. 이 차이를 구분하면 불필요한 useEffect와 state를 줄일 수 있습니다.
무한 렌더링 오류와 이 경고의 차이
Maximum update depth exceeded와 이번 경고는 서로 이어질 수 있지만 완전히 같은 문제는 아닙니다. 무한 렌더링 오류는 상태 변경이 반복되어 React가 더 이상 렌더를 진행하기 어렵다고 판단했을 때 나옵니다. 이번 경고는 렌더링 중 다른 컴포넌트를 업데이트했다는 타이밍 문제에 더 초점이 있습니다.
예를 들어 같은 컴포넌트 본문에서 매번 setCount(count + 1)를 호출하면 현재 컴포넌트가 계속 다시 렌더링됩니다. 반면 자식 컴포넌트 본문에서 부모의 setter를 호출하면 “자식 렌더링 중 부모를 업데이트했다”는 경고가 먼저 보일 수 있습니다. 결국 둘 다 렌더링 중 상태 변경이라는 공통점을 가지지만, 콘솔 메시지가 가리키는 문제 지점은 조금 다릅니다.
그래서 디버깅할 때는 경고 문구에 나온 컴포넌트 이름을 먼저 봐야 합니다. Cannot update a component (Parent) while rendering a different component (Child)처럼 표시된다면, Child 내부에서 Parent 상태를 바꾸는 호출을 찾는 것이 출발점입니다.
다시 발생하지 않게 확인할 체크포인트

이 경고를 다시 만나면 먼저 setState를 지우려고 하기보다 위치를 확인해야 합니다. 컴포넌트 함수 본문, JSX를 반환하기 전 조건문, map 안에서 바로 실행되는 함수, props로 받은 setter를 즉시 호출하는 코드가 있는지 살펴봅니다.
특히 자식 컴포넌트에서 부모의 setter를 props로 받았다면 호출 위치를 나눠서 봐야 합니다. 클릭, 변경, 제출 같은 이벤트 안에서 호출한다면 자연스러운 구조입니다. 반대로 컴포넌트 본문에서 조건문과 함께 호출한다면 렌더링 중 부모 state를 건드리는 코드일 가능성이 큽니다.
초기값을 맞추려는 코드도 자주 문제를 만듭니다. 목록의 첫 번째 값을 자동 선택해야 한다면 부모의 초기 state, 서버 데이터 로딩 이후 조건부 effect, 또는 선택값을 별도 state로 저장하지 않고 계산할 수 있는지부터 비교해야 합니다. 렌더링 중 보정하는 방식은 당장은 짧아 보여도 컴포넌트가 늘어나면 경고를 추적하기 어려워집니다.
마지막으로 useEffect로 옮겼다고 끝난 것은 아닙니다. effect 안에 상태 변경이 있다면 의존성 배열과 종료 조건을 같이 확인해야 합니다. 조건 없이 effect에서 상태를 바꾸면 경고는 사라져도 추가 렌더링이나 다른 루프가 생길 수 있습니다.
정리
Cannot update a component while rendering a different component 경고는 상태 변경 자체보다 호출 타이밍을 보라는 신호입니다. React가 컴포넌트를 렌더링하는 동안 다른 컴포넌트의 상태를 바꾸면, 현재 화면 계산이 끝나기 전에 새로운 업데이트가 끼어들게 됩니다.
사용자 행동으로 바뀌는 값은 이벤트 핸들러에 두고, 처음부터 정해질 수 있는 값은 부모의 초기 state에서 처리하는 것이 좋습니다. 외부 데이터가 들어온 뒤 맞춰야 하는 값이라면 useEffect를 검토하되, 종료 조건과 의존성 배열을 함께 확인해야 합니다.
다음에 같은 경고가 나오면 콘솔에 표시된 컴포넌트 이름을 기준으로 찾아가면 됩니다. 렌더링 중인 컴포넌트 안에서 부모나 다른 컴포넌트의 setter를 바로 호출하고 있는지 확인하는 것만으로도 원인의 상당 부분을 좁힐 수 있습니다.