Objects are not valid as a React child~ 에러와 Typescript

    반응형

    Object are not vaild 에러~

     

    간단한 기능을 가지고 있는 SNS를 만들어 보던 중에 Objects are not valid as a React child~ 에러가 발생했다. 이 에러를 해결하는 데 많은 시간을 보냈다. 간단하게 해결할 수 있음에도 많은 시간을 보내야 했던 이유와 해결 방법에 대해 글을 작성한다.

     

    에러발생


    Object are not Vaild ~~ Error

     

    ReactTypeScript, SCSS, Firebase를 사용해서 SNS 앱을 만드는 공부를 하고 있었는데 댓글에 관련한 컴포넌트를 만드는 과정에서 이러한 오류가 발생했다. 오류 내용을 살펴보면 "객체는 자식 컴포넌트로 직접 렌더링 할 수 없다" 라는 내용이다. 구글링을 통해 검색해 보니 React 프로젝트에서 많은 개발자들이 흔히 마주하는 오류 중 하나였고, 간단한 해결 방법들이 즐비해 있었다. 또한 에러 메시지에서 친절하게 {uid, createdAt, email, comment} 키를 가진 객체를 직접 렌더링 하지 마라 라고 설명까지 해주고 있으니 이때까지만해도 쉽게 해결할 수 있을 거라고 생각했다.

     

    기본적인 해결방법


    1. 객체를 문자열로 변환하기

    const App = () => {
     const dataObj = { text: "에러발생" }
     
     return (
      <div>
       {JSON.stringify(dataObj)}
      </div>
     )
    }
    
    export default App


    JSON.stringify( ) 를 통해 객체 데이터를 JSON 문자열 데이터로 변경해서 사용하면 Objects are not valid as a React child~ 에러가 발생하지 않는다.

     

    2. map( ) 을 사용한 렌더링

    const App = () {
        const comments = [
            { id: 1, name: 'minhoo', content: "hi" },
            { id: 2, name: 'gahyoun', content: "Nice to meet you" },
        ];
        
        return (
            <>
                {comments.map(comment => (
                    <div key={comment.id}>
                        <span>{comment.name}</span>
                        <span>{comment.age}</span>
                    </div>
                ))}
            </>
        );
    }
    
    export default App

     

    배열을 자식 요소로 직접 렌더링 하지 않고 map() 을 사용해 배열의 각 항목을 유효한 React 하위 요소로 변환하여 사용하면 Objects are not valid as a React child~ 오류가 발생하지 않는다.

     

    3. 조건부 렌더링을 통한 에러 방지

    const App = () {
        return (
            <div>
             {myObj && (<div>{myObj.name}</div>)}
            </div>
        );
    }
    
    export default App

     

    조건부 렌더링을 통해 객체가 유효한지를 먼저 판단한 후에 렌더링 될 수 있도록 하면 Objects are not valid as a React child~ 오류가 발생하지 않는다. 객체가 비어있거나 null 일 때 유용하게 사용할 수 있다.

     

    내가 마주한 문제


    우선 로직은 이러하다. CommentForm 컴포넌트를 통해 댓글을 작성하면 FireStore 데이터베이스에 comments라는 필드에 배열의 형태로 comment, uid, email, createdAt의 값이 입력될 수 있도록 했다. 

     

    export interface CommentFormProps {
      post: PostProps | null
    }
    
    const CommentForm = ({ post }: CommentFormProps ) => {
      const { user } = useContext(AuthContext)
    
      const [comment, setComment] = useState<string>("")
    
      const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
        const { target: { name, value } } = e;
    
        if(name === 'comment') { setComment(value) }
      }
    
      const onSubmit = async (e: any) => {
        e.preventDefault()
    
        if(post && user) {
          const postRef = doc(db, 'posts', post?.id)
    
          const commentObj = {
            comment: comment,
            uid: user?.uid,
            email: user?.email,
            createdAt: new Date()?.toLocaleDateString("ko", {
              hour: "2-digit",
              minute: "2-digit",
              second: "2-digit",
            }),
          };
    
          await updateDoc(postRef, {
            comments: arrayUnion(commentObj)
          })
    
          toast.success("댓글을 생성했습니다.")
          setComment("")
          
          try {
          } catch (e: any) {
            console.log(e)
          }
        }
      }
    
      return (
        <form className="post-form" onSubmit={onSubmit}>
          <textarea 
            className="post-form__textarea"
            placeholder="댓글로 의견을 남겨보세요!"
            name="comment"
            id="comment"
            required
            onChange={onChange}
            value={comment}
          />
    
          <div className="post-form__submit-area">
            <div />
            <input 
              type='submit'
              value="comment"
              className="post-form__submit-btn"
              disabled={!comment}
            />
          </div>
        </form>
      )
    }
    
    export default CommentForm

     

    CommentForm 컴포넌트를 통해 데이터베이스에 등록된 값들은 CommentBox 컴포넌트에서 댓글의 내용을 볼 수 있도록 한다. CommentBox 컴포넌트는 해당 포스트에 대한 데이터와 해당 포스터에 대한 댓글 데이터를 props로 받는다.

     

    export interface CommentProps {
      comment: string
      uid: string
      email: string
      createdAt: string
    }
    
    interface CommentBoxProps {
      data: CommentProps
      post: PostProps
    }
    
    const CommentBox = ({ data, post }: CommentBoxProps) => {
    
      
      const { user } = useContext(AuthContext)
    
      const handleDeleteComment = async () => {
        if(post) {
          try {
            const postRef = doc(db, 'posts', post?.id)
            await updateDoc(postRef, {
              comments: arrayRemove(data)
            })
    
            toast.success("댓글을 삭제했습니다.")
          } catch (e) {
            console.log(e)
          }
        }
      }
    
      return (
        <div className={styles.comment} key={data?.createdAt}>
          <div className={styles.comment__borderBox}>
    
            <div className={styles.comment__imgBox}>
              <div className={styles.comment__flexBox}>
                <img src='/logo192.png' alt="profile"/>
                <div className={styles.comment__email}>{data?.email}</div>
                <div className={styles.comment__createdAt}>{data?.createdAt}</div>
              </div>
            </div>
    
            <div className={styles.comment__content}>
              {data?.comment}
            </div>
    
          </div>
    
          <div className={styles.comment__submitDiv}>
            {data?.uid === user?.uid && (
              <button type="button" className="comment__delete-btn" onClick={handleDeleteComment}>
                삭제하기
              </button>
            )}
          </div>
        </div>
      )
    }
    
    export default CommentBox

     

    그리고 이러한 컴포넌트들은 PostDetail 컴포넌트에서 전체적으로 렌더링 되고 props를 넘겨주게 된다.  그래서 해당 포스트에 댓글 필드에 값이 있으면 CommetBox 컴포넌트를 통해 해당 포스트의 댓글을 렌더링 하는데 순서를 정렬하기 위해 배열의 값을 복사해 거꾸로 해서 최신순으로 렌더링 되게끔 한다.

     

    const PostDetail = () => {
      const [post, setPost] = useState<PostProps | null>(null)
    
      const params = useParams()
    
      const navigate = useNavigate()
    
      const getPost = useCallback(async () => {
        if(params.id) {
          const docRef = doc(db, 'posts', params.id)
    
          onSnapshot(docRef, (doc) => {
            setPost({...doc?.data() as PostProps, id: doc?.id})
          })
        }
      }, [params.id])
    
    
      useEffect(() => {
        if(params.id) { getPost() }
      },[params.id, getPost])
    
      return (
        <div>
          <div className="post__header">
            <button type="button" onClick={() => navigate(-1)}>뒤로가기</button>
          </div>
    
          { post ? (
            <>
              <PostBox post={post} />
              <CommentForm post={post} />
              {post?.comments
                ?.slice(0)
                ?.reverse()
                ?.map((data: CommentProps, index: number) => (
                  <CommentBox key={index} data={data} post={post} />
                ))}
            </>
          ) : (
            <div className='loader'>로딩중!!</div>
          )}
    
        </div>
      )
    }
    
    export default PostDetail

     

    이렇게 코드를 작성하고 동작하면 아래의 사진처럼 FireStore 데이터 베이스에 정상적으로 댓글이 입력된다. 

     

    FireStore Comments 값 입력

     

    문제는 댓글은 정상적으로 데이터베이스에 입력이 되는데 렌더링 되지 않는다는 문제였다. 코드를 전체적으로 살펴봐도 문제가 될 만한 상황을 발견하지 못했었다. map( )과 조건부 렌더링을 통해 정상적으로 데이터를 넣고 출력하는데도 오류가 발생해서 무엇 때문에 계속 Objects are not valid as a React child~ 오류가 발생하는지 알 수 없었다.

     

    해결방법


    이런 문제로 몇 시간을 꽁꽁 싸메고 있었다는 게 허탈할 정도로 해결 방법은 간단했다. 기존의 포스트에 대한 타입 정의를 interface를 통해 아래의 코드와 같이 정의했다.

    export interface PostProps {
        id: string
        email: string
        content: string
        createdAt: string
        uid: string
        profileUrl?: string
        likes?: string[]
        likeCount?: number
        comments?: any
        hashTags?: string[],
        imageUrl?: string
    }

     

    comments 부분의 타입을 정의하기 전이라 무슨 값이든 들어갈 수 있도록 any 타입으로 정의해두었는데 여기서 문제가 발생했다. 구글링 통해 찾아보니 타입스크립트에서 any 타입으로 지정하게 되면 정확히 무슨 타입인지 구별할 수 없다고 한다는 글을 발견했다. 그래서 CommentBox에서 정의한 CommentProps[ ] 타입으로 comments의 정의를 변경했더니 정상적으로 댓글들이 렌더링 되는 모습을 볼 수 있었다.

    export interface PostProps {
        id: string
        email: string
        content: string
        createdAt: string
        uid: string
        profileUrl?: string
        likes?: string[]
        likeCount?: number
        comments?: CommentProps[]
        hashTags?: string[],
        imageUrl?: string
    }

     

    하지만 아직도 풀리지 않는 의문이 한 가지 있는데 강의를 보면서 강사님은 그대로 comments의 값을 any로 작성해도 정상적으로 출력이 되는데 왜 나는 에러가 발생하는 것인가에 대한 의문은 풀리지 않는다.. 의문에 답을 찾는다면 다시 글을 수정해야겠다!

     

     

     

    반응형

    댓글