React Objects are not valid as a React child 에러 원인과 해결법

React Objects are not valid as a React child 에러 원인과 해결 가이드

3초 요약: 이렇게 고치세요
  • 원인: JSX 중괄호 안에 문자열·숫자가 아닌 객체를 그대로 넣어서 발생 — React는 객체를 화면에 어떻게 그려야 할지 알 수 없음
  • 해결: 객체면 user.name처럼 원시값 속성을 꺼내 쓰고 / 배열이면 .map()으로 펼치고 / DatetoLocaleDateString()으로 문자열 변환
  • 주의: 에러 메시지의 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는 그런 조용한 실패 대신 에러를 던져서 문제를 즉시 알려주는 쪽을 택한 겁니다.

실무에서 이 에러가 나오는 상황은 대체로 아래 세 가지입니다.

  1. API 응답 구조가 바뀌어서, 예전엔 문자열이던 필드가 객체로 내려오는 경우
  2. 배열을 .map() 없이 그대로 중괄호에 넣어 각 항목(객체)이 통째로 렌더링 대상이 되는 경우
  3. 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 개발 입문 블로그] 에서 볼 수 있습니다.

댓글

이 블로그의 인기 게시물

TypeScript: Unknown vs Any 타입 차이점과 안전한 타입 가드(Type Guard)

Next.js 하이드레이션 오류 원인과 해결법 완벽 정리

Next.js App Router에서 getServerSideProps 대체 및 데이터 페칭법