React.FC 자체를 props로 넘기는 방법

CDD·2024년 1월 29일
0

고민을 하게 된 계기

서론

본격적으로 회사에서 프론트 개발을 한지 4개월 차가 넘어가는데, 아직도 React.js에 대한 구조적 고민은 계속해서 하게 되는 것 같다. 이번에 제목과 같은 고민을 하게 된 이유는 아이콘을 렌더링하는 컴포넌트에서 동적으로 아이콘을 받아 넣고 싶은 상황이었는데, 아이콘을 React.FC와 같은 형태로 관리하다보니 props에 꽤나 부담을 주는 것은 아닐까라는 생각이 들었다.

const Section = (props: SectionProps) => {
  return (
    <Link
      className={cn(props.className || "", "h-full")}
      href={props.redirectURL}
    >
      <div className={"flex flex-col justify-center items-center gap-8 "}>
        {/* 여기에 아이콘을 넣고 싶음 */}
        <div className={cn("h-full flex justify-center items-center")}>
          {props.message}
        </div>
      </div>
    </Link>
  );
};

How Do you think, GPT?

리액트에서 컴포넌트 자체를 props로 전달하는 것은 특별한 경우에 사용될 수 있습니다. 이는 일반적으로 고급 사용 사례 중 하나이며, 주의를 기울여야 합니다. 일반적으로 컴포넌트는 데이터(props)를 통해 정보를 전달받고, 해당 데이터를 기반으로 렌더링을 수행합니다. 그러나 때로는 컴포넌트 자체를 다른 컴포넌트에게 전달하고, 해당 컴포넌트를 그 자체로 사용하거나 조작하는 경우가 있을 수 있습니다. 예를 들어, 고차 컴포넌트(Higher-Order Component)나 래더 함수(Render Prop)를 사용하여 특정 로직이나 상태를 재사용하고 싶을 때, 컴포넌트 자체를 props로 전달할 수 있습니다.

여기서 주의점은 props로 받아온 컴포넌트를 실사용하는 상황에서는 특정 로직이나 상태를 재사용하기 어렵다는 것을 의미하는 듯하다. 내가 걱정했던 부분은 무거운 컴포넌트를 props로 넘겨주었을 때의 문제였는데, 예상 밖의 답이 나왔다.

props에 대한 생각의 변화

🥚 props를 잘 활용하면 모든 컴포넌트가 이어질 수 있다. 그러므로 사용법을 익히자.

리액트를 처음 배웠을 때 props라는 개념이 되게 신기했다. 서로 다른 파일 간에 props를 활용하여 데이터를 전달할 수 있다는 사실을 알게 된 이후부터는 그 어떤 웹사이트도 만들 수 있을 것 같았다. 바닐라로 개발할 때 겪었던 답답한 점이 해결되기도 했고 말이다.

🐣 상태관리 라이브러리를 사용하면 props를 안써도 된다고? props drilling을 방지할 수 있다고?

리액트를 좀 더 사용해보면서 느꼈던 문제점은 props를 타고, 타고 들어가는 경우가 많아 계속해서 선언을 해줘야 한다는 점이었다. 그리고 중간의 특정 컴포넌트가 해당 데이터를 사용하지 않는 경우가 있다고 하더라도 계속해서 다음 props로 넘겨줘야 하는 경우도 있었다. 그때 알게되었던 Redux, Recoil 등의 상태 변화 라이브러리들은 이러한 고민에서 해방시켜주었고, 딱 원하는 컴포넌트에서 아름다운 사용을 도와주었다. 이 당시에 props로 관리하기 귀찮은 데이터들은 모두 상태관리 라이브러리로 뺐던 것 같다.

하지만 이 생각은 오래가지 못했다. 상태가 너무 많아지니 오히려 상태선언 코드들이 무지하게 많아지고, 코드를 작성하는 상황에서 이게 어느 스코프 상에서 존재하는 상태인지도 한눈에 알아보기가 힘들었다. 더구나 전역으로 값을 관리한다는 말은 전역 변수를 사용하는 것과 동일한데, 그렇기에 크기가 큰 데이터를 전역에다가 올려두는 것이 좋은 방법일 것 같진 않았다. 그래서 이 시점에서는 propsstate를 적절히 묶는 방법을 여러번 고민했었다.

🐥 Atomic Design을 사용하고 있으니, 효율적인 props의 사용법에 대해 고민해보자.

사이드 프로젝트를 하면서 디자인 패턴을 적용하고 싶은 욕심이 생겼다. 제일 눈에 들어온 게 Atomic이고, 재사용성을 위해 props를 제법 많이 사용하게 되었다. 여기서 느꼈던 문제점은 렌더링 방식 자체를 분기처리 하는 것이 힘들다는 것이었다. 개발 잘하는 친구들에게 물어보니 애초에 분기처리를 해서 특정 컴포넌트를 렌더한다는 것 자체가 애초에 잘못된 추상화이고, animal이라는 클래스에 개가 짖는 방식, 닭이 우는 방식 등이 다 담겨져있는 것 같다는 말을 하더라 🥲.

{props.type === "artists" && (
          <ArtistPostModal
            open={postModalStatus}
            setOpen={setPostModalStatus}
          />
        )}
        {props.type === "albums" && (
          <AlbumPostModal open={postModalStatus} setOpen={setPostModalStatus} />
        )}

이때 좀 추상화에 개념에 대해 흥미가 생기기 시작했다. 코드를 보면서 설명하는 것이 좋을 것 같은데, 현재 구조에 대해서 간단하게 설명하자면 아티스트, 앨범을 관리하는 컴포넌트를 하나로 통합해서 관리하고자 했다. 그래서 상위에서 type을 던져주고, 받는 입장에서는 타입을 보고 다른 Modal을 렌더하도록 말이다. 하지만 이렇게 코드를 짜게 된다면 계속해서 통합 컴포넌트의 크기만 커질 것이고, 아티스트 파트를 수정하다가 앨범 파트 쪽에 문제가 생길 확률도 높아보였다.

이전에는 이런식으로 하위 컴포넌트 입장에서 렌더링을 했다면 지금은 방식을 바꿔 상위 컴포넌트를 활용하기로 했다.

const handleModifyModalOpen = (artist: ArtistImageGridType) => {
    setDetailId(artist.id);

    if (artist.artistType === "SOLO") {
      setArtistModifyModalOpen(true);
      return;
    }
    if (artist.artistType === "GROUP") {
      setGroupModifyModalOpen(true);
      return;
    }
  };

  return (
    <>
      <Management<ArtistImageGridType>
        label={"아티스트"}
        type={"artists"}
        handlePostModalOpen={handlePostModalOpen}
        handleModifyModalOpen={handleModifyModalOpen}
        fetch={getArtist}
        search={searchArtist}
        // detail={getArtistDetail}
      />
      <ArtistPostModal open={postModalOpen} setOpen={setPostModalOpen} />
      <ArtistModifyModal
        id={detailId}
        open={artistModifyModalOpen}
        setOpen={setArtistModifyModalOpen}
      />
      <GroupModifyModal
        id={detailId}
        open={groupModifyModalOpen}
        setOpen={setGroupModifyModalOpen}
      />
    </>
  );

이렇게 해주니 Management 컴포넌트의 무게가 가벼워졌고, 재사용성도 높아진 것을 확인할 수 있었다. 모달과 관련된 로직들은 상위에 묶여있으니 하위 컴포넌트에서는 깔끔하게 props로 받아온 setModalOpen 함수만 호출해주면 되는 것이다. 이 타이밍에서 문뜩 고민이 하나 들었는데, '컴포넌트 자체를 하위로 넘겨줄 수 있으면 하위 컴포넌트에서 id 값을 관리하고 편하게 모달을 오픈할 수 있지 않을까?' 라는 생각이 들었다.

사실 이 경우는 아티스트그룹 간의 모달 개수와 형식이 너무 달라서 적용하기 힘들었던 개념이긴 한데, 새롭게 이것이 필요한 상황이 생겨났다. 그리고 이 경우가 바로 처음에 봤던 이 코드이다.

const Section = (props: SectionProps) => {
  return (
    <Link
      className={cn(props.className || "", "h-full")}
      href={props.redirectURL}
    >
      <div className={"flex flex-col justify-center items-center gap-8 "}>
        {/* 여기에 아이콘을 넣고 싶음 */}
        <div className={cn("h-full flex justify-center items-center")}>
          {props.message}
        </div>
      </div>
    </Link>
  );
};

단순히 머릿 속에서 떠오르는 해결책들이 몇가지 있었는데, 아래 코드와 같다.

// 상위 컴포넌트 호출 방식
<하위컴포넌트 icon={<아이콘컴포넌트 />} />
  
// 하위 컴포넌트 수신 방식
interface 하위컴포넌트props {
  ...
  icon: React.FC // Node, ComponentType 등이 들어갈수도 있을 것 같음
}

위와 같은 방식으로 하면 아이콘 컴포넌트의 props를 상위 컴포넌트에서 선언해야 하기 때문에 개인적인 불편함이 있었다. 그래서 두 번째 방법은 props로 받는 아이콘의 타입을 React.FC로 사용하고, icon={아이콘컴포넌트}와 같은 방식으로 사용하는 것이었다. <>을 상위에서 사용하느냐, 하위에서 사용하느냐의 차이인 것 같은데, 상위에서 사용한다면 하위에서는 {props.icon} 이렇게만 적어줘도 되고, 상위에서 사용하지 않을 땐 <props.icon className="..." /> 이런 식으로 사용해줘야 한다.

// 상위 컴포넌트 렌더 파트

<Section
![](https://velog.velcdn.com/images/dev_cdd/post/ae96161d-9fd5-4588-93c6-bd337ed8b294/image.png)
        icon={UsersIcon}
        className={"w-[50%]"}
        message={"아티스트 관리"}
        redirectURL={"/admin/artist"}
      />

// 하위 컴포넌트 렌더 파트
          
import Link from "next/link";
import { cn } from "@/lib/utils";
import React, { ComponentType } from "react";

interface SectionProps {
  className?: string;
  icon: React.FC<{ className: string }>;
  message: string;
  redirectURL: string;
}

const Section = (props: SectionProps) => {
  return (
    <Link
      className={cn(props.className || "", "h-full")}
      href={props.redirectURL}
    >
      {/*{props.icon && <props.icon className={"text-hipzip-white"} />}*/}
      <div className={"flex flex-col justify-center items-center gap-8 "}>
        <props.icon
          className={
            "text-hipzip-white h-64 w-64 hover:scale-110 transition-transform"
          }
        />
        <div className={cn("h-full flex justify-center items-center")}>
          {props.message}
        </div>
      </div>
    </Link>
  );
};

export default Section;

이렇게 완성시킨 코드고, props로 태그 없이 넘긴다고 하면 참조값과 같은 형식으로 넘어갈 것이기 때문에 막 "무거운 컴포넌트를 어떻게 하위로 내려줄수가 있어? 너 정말 나쁘다 😭" 와 같이 생각할 필요는 없는 것이다. 사실 타입과 렌더링 관련해서 오류가 많이 났어서 고치는데 어려움을 조금 겪었었고, 위의 글을 참고한다면 쉽게 props를 넘겨 사용해줄 수 있을 것이다.

0개의 댓글