본문 바로가기

공부/트러블 슈팅

[트러블 슈팅] 공통 헤더 개발 중 발생한 Typescript 오류 (ReactNode, ReactElement)

반응형

발생한 오류 🔥

Type error: Property 'props' does not exist on type 'string | number | bigint | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<...>'. Property 'props' does not exist on type 'string'.

 

공통 헤더를 만드는 과정에서 `leftIcon` props 속성에 접근할 때 위 오류가 발생했습니다.

'use client';

import { cn } from '@src/utils';
import { useRouter } from 'next/navigation';
import React from 'react';

interface HeaderProps {
  leftIcon: React.ReactNode;
  title: string;
  rightIcon: React.ReactNode;
  className: string;
}

const Header = ({ leftIcon, title, rightIcon, className }: Partial<HeaderProps>) => {
  const router = useRouter();

  const onClickLeftIcon = () => {
    const isArrow = leftIcon?.props['aria-label'] === 'arrow'; // 🔥 여기서 오류 발생

    if (isArrow) {
      router.back();
    } else {
      router.push('/');
    }
  };

  return (
    <header className={cn('flex w-full items-center justify-between bg-white p-4', className)}>
      {/* 왼쪽 아이콘 */}
      <div className="flex h-6 w-6 items-center">
        {leftIcon ? <button onClick={onClickLeftIcon}>{leftIcon}</button> : <div className="w-6" />}
      </div>

      {/* 중앙 텍스트 */}
      <div>{title ? <span className="font-semiBold text-gray-900 font-body-1">{title}</span> : <div />}</div>

      {/* 오른쪽 아이콘 */}
      <div className="flex h-6 w-6 items-center">
        {rightIcon ? <button>{rightIcon}</button> : <div className="w-6" />}
      </div>
    </header>
  );
};

export default React.memo(Header);

 

해결 과정 🔎

원인 분석

 

`leftIcon`의 타입이 ReactNode인데 Typescript가 `leftIcon`을 `string | number | ReactElement | ...`로 인식했고 string과 같은 타입에는 `props` 속성이 존재하지 않기 때문에 발생한 오류였습니다.

 

ReactNode와 ReactElement의 차이

 

`ReactNode`와 `ReactElement`는 React에서 유사해 보이지만 실제로는 다른 개념을 나타냅니다.

 

ReactNode

  • React에서 렌더링 가능한 모든 값을 포함합니다.
  • string, number, ReactElement, null, undefined, boolean, 그리고 이들의 배열도 포함됩니다.

ReactElement

  • React에서 실제로 컴포넌트를 표현하는 객체입니다. JSX로 작성된 컴포넌트는 항상 ReactElement 타입입니다.
  • 예: <div /> 또는 <CustomComponent />

따라서 `ReactNode`는 `ReactElement`보다 더 포괄적인 타입이며, `props` 속성은 `ReactElement`에만 존재합니다.

 

해결 방법 ✨

`React.isValidElement`를 사용해 `ReactNode`가 실제로 `ReactElement`인지 확인 후 접근하는 방법을 선택했습니다.

 

수정 전 코드

const onClickLeftIcon = () => {
  const isArrow = leftIcon?.props['aria-label'] === 'arrow';

  if (isArrow) {
    router.back();
  } else {
    router.push('/');
  }
};

 

수정 후 코드

const onClickLeftIcon = () => {
  const isArrow = React.isValidElement(leftIcon) && leftIcon.props['aria-label'] === 'arrow';

  if (isArrow) {
    router.back();
  } else {
    router.push('/');
  }
};

 

`React.isValidElement`가 런타임에서 타입 가드 역할을 수행하여, `props` 속성에 안전하게 접근할 수 있도록 보장해줍니다.

 

코드 설명

 

1. React.isValidElement(leftIcon)

  • leftIcon이 ReactElement인지 확인 후 Typescript는 leftIcon이 ReactElement임을 추론합니다.

2. leftIcon.props['aria-label']

  • React.isValidElement를 통해 타입이 좁혀져서 props 속성에 접근해도 오류가 발생하지 않습니다.

 

회고 🧐

React와 Typescript가 제공하는 타입에 대해서 공부할 수 있었고 타입 가드를 적절히 활용해 문제를 해결할 수 있었습니다.

Typescript를 활용한 안정적인 컴포넌트 설계의 중요성을 다시 한번 느끼게 되는 것 같습니다!

 

아직 많이 부족하기 때문에 조언은 언제나 환영입니다 :)

반응형