React Objects are not valid as a React child 에러 원인과 해결법
- 원인: JSX 중괄호 안에 문자열·숫자가 아닌 객체를 그대로 넣어서 발생 — React는 객체를 화면에 어떻게 그려야 할지 알 수 없음
- 해결: 객체면
user.name처럼 원시값 속성을 꺼내 쓰고 / 배열이면.map()으로 펼치고 /Date면toLocaleDateString()으로 문자열 변환 - 주의: 에러 메시지의
found: object with keys {...}부분에 실제 객체의 키가 찍히므로 범인을 특정하는 단서가 됨 — 단Date처럼 자체 속성이 없는 객체는 이 목록이 비어 보임
<p>{user}</p> {/* 에러 */}
<p>{user.name}</p> {/* 정상 */}
↓ 왜 이런 에러가 나는지 원리 보기
잘 돌아가던 화면이 갑자기 하얗게 날아가면서 "Objects are not valid as a React child (found: object with keys {name, email})" 같은 에러가 뜬 적 있으실 겁니다. 원인은 대부분 간단합니다. JSX 중괄호 안에 문자열이나 숫자가 아닌 객체를 그대로 넣었기 때문인데, 정작 어느 줄이 범인인지 찾는 게 더 오래 걸리는 에러입니다.
React가 객체를 화면에 그리지 못하는 이유
JSX의 중괄호는 그 안의 값을 화면에 출력하라는 뜻입니다. React는 여기에 들어온 값이 문자열이나 숫자면 텍스트 노드로 만들고, 배열이면 각 항목을 순회하고, React 엘리먼트면 그대로 렌더링합니다. 그런데 일반 객체가 들어오면 얘기가 달라집니다. { name: '김개발', email: 'dev@example.com' }이라는 객체를 화면에 어떤 글자로 표시해야 할지는 React가 정할 수 없는 문제이기 때문입니다.
자바스크립트라면 String(obj)처럼 [object Object]로 대충 변환할 수도 있었겠지만, 그렇게 하면 화면에 의미 없는 문자열이 조용히 찍히고 개발자는 왜 이상한 글자가 나오는지 한참 헤매게 됩니다. React는 그런 조용한 실패 대신 에러를 던져서 문제를 즉시 알려주는 쪽을 택한 겁니다.
실무에서 이 에러가 나오는 상황은 대체로 아래 세 가지입니다.
- API 응답 구조가 바뀌어서, 예전엔 문자열이던 필드가 객체로 내려오는 경우
- 배열을
.map()없이 그대로 중괄호에 넣어 각 항목(객체)이 통째로 렌더링 대상이 되는 경우 Date객체를 포맷 변환 없이 그대로 출력하려는 경우
예전에 외주로 참여했던 커뮤니티 서비스에서 이 에러를 만난 적이 있습니다. 게시글 목록에 작성자 이름을 {post.author}로 뿌리고 있었는데, 어느 날 배포하고 나니 목록 페이지가 통째로 하얗게 떴습니다. 프런트엔드 코드는 그 주에 건드린 게 없어서 처음엔 빌드가 꼬였나 싶어 캐시 지우고 다시 배포해봤는데 똑같았습니다. 알고 보니 백엔드에서 작성자 프로필 이미지를 추가하면서 author를 문자열에서 { name, avatarUrl } 객체로 바꿨던 거였습니다. 프런트엔드는 그대로인데 데이터 모양만 바뀌어서 터진 케이스라, 코드 히스토리만 뒤져서는 원인을 찾기가 어려웠습니다.
헷갈리기 쉬운 지점: 에러 메시지가 범인을 알려준다
이 에러는 컴포넌트 이름 대신 데이터 얘기만 해서 어디를 고쳐야 할지 막막해 보이지만, 메시지 안의 found: object with keys {name, avatarUrl} 부분이 좋은 단서가 됩니다. 여기 찍힌 키 조합을 그대로 코드에서 검색하면 어떤 데이터가 문제인지 바로 좁힐 수 있습니다. 위 사례에서도 avatarUrl이라는 낯선 키가 찍힌 걸 보고 나서야 프런트엔드가 아니라 응답 데이터가 바뀌었다는 걸 알아챘습니다. 다만 이 방법이 항상 통하는 건 아닌데, 어떤 경우에 키 목록이 비어버리는지는 아래 케이스 3에서 다룹니다.
실전 해결 코드: 케이스별 처리 방법
케이스 1: 객체를 그대로 출력한 경우
const post = {
title: '리액트 렌더링 정리',
author: { name: '김개발', avatarUrl: '/avatar.png' },
};
// 문제: author가 객체라 그대로 렌더링할 수 없음
<span>{post.author}</span>
// 해결: 화면에 표시할 원시값 속성을 꺼내서 사용
<span>{post.author.name}</span>
// 응답 구조가 바뀔 가능성이 있다면 옵셔널 체이닝으로 방어
<span>{post.author?.name ?? '알 수 없음'}</span>
케이스 2: 배열을 map 없이 출력한 경우
const tags = [
{ id: 1, label: 'React' },
{ id: 2, label: 'TypeScript' },
];
// 문제: 배열 자체는 통과하지만 안의 각 객체에서 에러 발생
<div>{tags}</div>
// 해결: map으로 펼치면서 원시값만 렌더링
<div>
{tags.map((tag) => (
<span key={tag.id}>{tag.label}</span>
))}
</div>
배열 자체는 React가 순회할 수 있는 값이라 에러 메시지가 배열을 가리키지 않습니다. 대신 그 안에 들어있던 객체 하나의 키가 찍히기 때문에, 문자열 배열이었다면 잘 나왔을 코드가 객체 배열로 바뀌는 순간 터지는 식으로 나타납니다.
케이스 3: Date 객체를 그대로 출력한 경우
const createdAt = new Date(post.createdAt);
// 문제: Date도 객체이므로 그대로 렌더링할 수 없음
<time>{createdAt}</time>
// 해결: 문자열로 변환해서 출력
<time>{createdAt.toLocaleDateString('ko-KR')}</time>
API에서 createdAt이 ISO 문자열로 내려올 때는 그냥 출력해도 잘 나오다가, 중간에 new Date()로 감싸는 로직이 들어가는 순간 이 에러가 납니다. 게다가 Date는 열거 가능한 자체 속성이 없어서, 에러 메시지의 키 목록이 found: object with keys {}처럼 비어 있게 나옵니다. 앞서 소개한 "키를 보고 범인을 찾는 방법"이 이 케이스에서는 통하지 않는 셈인데, 반대로 생각하면 키 목록이 비어 있다는 것 자체가 날짜 관련 코드를 의심할 단서가 됩니다.
디버깅 팁: 임시로 구조를 확인하기
// 데이터가 어떤 모양인지 화면에서 바로 확인하고 싶을 때
<pre>{JSON.stringify(post.author, null, 2)}</pre>
JSON.stringify는 객체를 문자열로 바꿔주므로 에러 없이 렌더링됩니다. 어떤 필드가 어떤 모양으로 들어오는지 확인하는 용도로는 편하지만, 실제 화면에 그대로 남겨두는 코드는 아니니 원인을 파악한 뒤에는 제대로 된 속성 접근으로 바꿔야 합니다.
JSX 중괄호에 넣은 값별 렌더링 결과
어떤 값이 통과하고 어떤 값이 에러를 내는지 정리했습니다.
| 값의 종류 | 렌더링 결과 | 비고 |
|---|---|---|
| 문자열 · 숫자 | 그대로 텍스트로 출력 | 가장 기본이 되는 정상 케이스 |
| null · undefined · false | 아무것도 렌더링하지 않음 | 조건부 렌더링에서 활용되는 동작 |
| 배열 | 각 항목을 순회하며 렌더링 | 항목이 객체면 그 지점에서 에러 발생 |
| 일반 객체 · Date | 에러 발생 | 속성 접근이나 문자열 변환을 거쳐야 함 (Date는 키 목록이 비어 있게 표시됨) |
JSX에서 중괄호가 어떤 값을 받아들이는지, 리스트를 어떻게 렌더링하는지는 공식 문서에서 확인할 수 있습니다.
JSX 렌더링 규칙과 리스트 처리 패턴을 좀 더 자세히 다룬 글은 [호마다의 IT 개발 입문 블로그] 에서 볼 수 있습니다.
댓글
댓글 쓰기