본격적으로 회사에서 프론트 개발을 한지 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>
);
};
리액트에서 컴포넌트 자체를 props로 전달하는 것은 특별한 경우에 사용될 수 있습니다. 이는 일반적으로 고급 사용 사례 중 하나이며, 주의를 기울여야 합니다. 일반적으로 컴포넌트는 데이터(props)를 통해 정보를 전달받고, 해당 데이터를 기반으로 렌더링을 수행합니다. 그러나 때로는 컴포넌트 자체를 다른 컴포넌트에게 전달하고, 해당 컴포넌트를 그 자체로 사용하거나 조작하는 경우가 있을 수 있습니다. 예를 들어, 고차 컴포넌트(Higher-Order Component)나 래더 함수(Render Prop)를 사용하여 특정 로직이나 상태를 재사용하고 싶을 때, 컴포넌트 자체를 props로 전달할 수 있습니다.
여기서 주의점은 props
로 받아온 컴포넌트를 실사용하는 상황에서는 특정 로직이나 상태를 재사용하기 어렵다는 것을 의미하는 듯하다. 내가 걱정했던 부분은 무거운 컴포넌트를 props
로 넘겨주었을 때의 문제였는데, 예상 밖의 답이 나왔다.
리액트를 처음 배웠을 때 props
라는 개념이 되게 신기했다. 서로 다른 파일 간에 props
를 활용하여 데이터를 전달할 수 있다는 사실을 알게 된 이후부터는 그 어떤 웹사이트도 만들 수 있을 것 같았다. 바닐라로 개발할 때 겪었던 답답한 점이 해결되기도 했고 말이다.
리액트를 좀 더 사용해보면서 느꼈던 문제점은 props
를 타고, 타고 들어가는 경우가 많아 계속해서 선언을 해줘야 한다는 점이었다. 그리고 중간의 특정 컴포넌트가 해당 데이터를 사용하지 않는 경우가 있다고 하더라도 계속해서 다음 props
로 넘겨줘야 하는 경우도 있었다. 그때 알게되었던 Redux, Recoil
등의 상태 변화 라이브러리들은 이러한 고민에서 해방시켜주었고, 딱 원하는 컴포넌트에서 아름다운 사용을 도와주었다. 이 당시에 props
로 관리하기 귀찮은 데이터들은 모두 상태관리 라이브러리로 뺐던 것 같다.
하지만 이 생각은 오래가지 못했다. 상태가 너무 많아지니 오히려 상태선언 코드들이 무지하게 많아지고, 코드를 작성하는 상황에서 이게 어느 스코프 상에서 존재하는 상태인지도 한눈에 알아보기가 힘들었다. 더구나 전역으로 값을 관리한다는 말은 전역 변수를 사용하는 것과 동일한데, 그렇기에 크기가 큰 데이터를 전역에다가 올려두는 것이 좋은 방법일 것 같진 않았다. 그래서 이 시점에서는 props
와 state
를 적절히 묶는 방법을 여러번 고민했었다.
사이드 프로젝트를 하면서 디자인 패턴을 적용하고 싶은 욕심이 생겼다. 제일 눈에 들어온 게 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
를 넘겨 사용해줄 수 있을 것이다.