우선 기능 개발부터 하고 컴포넌트에 대한 고민은 나중에 하자란 생각으로 사이드 프로젝트를 진행하던 중
구독중이던 매일매일 사이트에서 오늘의 질문으로 컴포넌트에 대한 질문을 발견했다.
이후 예전 리액트를 처음 시작했을 때 컴포넌트란 무엇일까? 하고 봤던 유튜브를 보며 커피를 먹던 중
내가 만들었던 매우 간단한 컴포넌트에도 적용 할 수 있을 거 같아서 부랴부랴 작업해봤다.
토스ㅣSLASH 22 - Effective Component 지속 가능한 성장과 컴포넌트 - YouTube[1]
아주 좋은 동영상인 거 같다. 리액트를 처음 배울 때 보았을 때엔 잘 이해가 안갔지만
영상의 주요 주제는 변화에 대처하기 쉬운 컴포넌트에 대한 이야기이다.
정말 별거 아닌 컴포넌트이다.
const ReferenceListWidgetTitle = ({
count,
children,
}: {
count: number;
children: React.ReactNode;
}) => (
<h2 className="text-base">
{children}
<span className="text-[0.8rem] text-[#a0a0a0] ml-1">({count})</span>
</h2>
);
// 사용 예시
<ReferenceListWidgetTitle count={attachedReferenceList.length}>
글에 첨부된 레퍼런스
</ReferenceListWidgetTitle>
사용 할 때에는 제목에서 사용할 제목과 뒤에 표기 할 ()
내부에 들어 갈 숫자를 props
로 넘겨주는 형태이다.
영상에서 말하는 좋은 컴포넌트의 기준 중 위 컴포넌트에게 해당되는 부분을 체크해보았다.
이런 수정 사항이 존재한다고 해보자
만약 사용처에서 ()
에 들어갈 문구의 색을 변경하고 싶다면? ()
글자가 제목으로부터 마진값을 얼마나 더 주고 싶다면?
두 가지 방법이 있을 거 같다.
Reference ... Title2
컴포넌트 생성 count props
를 받아 렌더링 되는 jsx
에게 넘겨 줄 스타일을 props
로 새로 받음 만약 1번 방식을 선택한다면 유연하게 대처하지 못하는 거라 볼 수 있다. 저 방법은 최악이다.
2번 방법을 선택했다고 가정 해보자
const ReferenceListWidgetTitle = ({
...
countClassName = ''
}: {
...
countClassName ?: string;
children: React.ReactNode;
}) => (
<h2 className="text-base">
{children}
<span className=`text-[0.8rem] text-[#a0a0a0] ml-1 ${countClassName}`>({count})</span>
</h2>
);
1번 방식보다는 유연하다고 볼 수 있을 거 같다. 최악보단 차악이지만 ..
컴포넌트가 특정 도메인과 상관 없는 props
등을 받을 때 재사용성이 올라가고
처음 보는 사람의 입장에서도 어떤 식으로 동작할지를 잘 드러낼 수 있다고 동영상에서 이야기 한다.
count
라는 props
명은 개발한 내 입장에서는 아 저 컴포넌트는 제목과 함께 특정 배열의 길이를 조그만 글씨로 렌더링 할거예요 ~ 라고 생각하지만
처음 보는 사람입장에선 도대체 저 props
가 뭘지 , 저 안에선 어떤 일이 일어날지 파악 하기가 힘들다.
이에 props
명을 도메인 로직과 상관 없는 일반적인 이름으로 바꿔보자
// 이전 사용 예시
<ReferenceListWidgetTitle count={attachedReferenceList.length}>
글에 첨부된 레퍼런스
</ReferenceListWidgetTitle>
// 변경 이후 사용 예시
<ReferenceListWidgetTitle subText={attachedReferenceList.length}>
글에 첨부된 레퍼런스
</ReferenceListWidgetTitle>
count
라는 특정 의미를 가진 props
명에서 일반적인 이름으로 바꿈으로서 내부에서 동작할 동작을 조금이나마 이해하기 쉬워졌다.
내 사이드 프로젝트는 워낙 사이즈가 작기 때문에 재사용성이 크게 중요하진 않지만 그래도 경험삼아 더 바라보았다.
만약 subText
가 있는 경우도 있고 없는 경우가 있다면? subText를 두 개 이상 좌우로 넣어야 한다면? 등등 다양한 요구 사항이 있을 때 위에서 언급했던 Component...1, 2,3
등등 매번 새로운 컴포넌트를 만드는 방법이 있을 것이다.
매번 새롭게 컴포넌트를 만드는 것은 최악이니까 안한다고 하고 저 안에 수 많은 조건문과 컨디셔널 props등을 넣어 대처한다고 해보자
그렇게 하면 요구사항은 반영 할 수 있겠지만 조건문이 떡칠 된 컴포넌트 내부는 걷잡을 수 없이 지저분해질 것이다.
하나의 함수가 한 가지 일만 해야 한다는 이론이나, 순수 함수에 대한 다양한 이론이 있지만 그 이론들을 모두 설명하기에는 내가 아직 너무 멍청하다.
하지만 그런 방법들이 좋은게 아니란건 알 수 있다.
좀 더 본질적으로 들어가서, 저게 컴포넌트로 만들어야 할 만큼 복잡한지? 추상화를 함으로서 얻는 이점이 무엇일지? 에 대해 생각해봐야했다.
그냥 저 부분을 굳이 컴포넌트로 안만들고 이렇게 만들면 어떤 식으로 렌더링 되는지, 추가 할 사항이 있다면 jsx 등을 직접 수정하면 되긴 하니까 가장 유연한 상태이지 않을까?
<h2 className="text-base">
어쩌구 제목 어쩌구 저쩌구
<span className=`text-[0.8rem] text-[#a0a0a0] ml-1 ${countClassName}`>({어쩌구.length})</span>
</h2>
굳이 추상화 해도 되지 않을 만큼의 간단한걸 불필요한 추상화로 내가 일을 복잡하게 만든게 아닐까? 라는 결론에 도달했다.
그래서 해당 부분등을 다음과 같이 수정했다.
<div className="flex gap-1 items-center">
<Heading h2>글에 첨부되지 않은 레퍼런스</Heading>
<Text span type="secondary">
({unAttachedReferenceList.length})
</Text>
</div>
디자인 시스템으로 사용 할 가장 저수준의 Heading,Text
컴포넌트 등을 이용하여 단순히 디자인 시스템 상 사용 할 props
와 렌더링 할 children
만을 이용하여
원하는 요소들을 표현 할 수 있도록 해주었다.
만약 다른 요소가 추가 되어야 한다면 만들었던 Heading, Text
등을 추가하면 될 것이고 새로운 스타일을 보여주고 싶다면 부가적인 className
을 수정이 일어나야 할 부분에 건내주기만 하면 될 것이다.
이전과 비교해서 봐보자
// 이전 사용 예시
<ReferenceListWidgetTitle count={attachedReferenceList.length}>
글에 첨부된 레퍼런스
</ReferenceListWidgetTitle>
// 변경 이후 사용 예시
<div className="flex gap-1 items-center">
<Heading h2>글에 첨부되지 않은 레퍼런스</Heading>
<Text span type="secondary">
({unAttachedReferenceList.length})
</Text>
</div>
정답은 없다고 하지만 내 기준에선 이전보단 더 나아진 거 같다.
지금 생각해보면 그냥 컴포넌트를 하나 지우고 모두 jsx
로 적어준것과 다를게 없긴 하지만
그래도 내가 이전에 짠 코드가 왜 별로인지에 생각 할 수 있어서 좋았다.
정말 추상화하고자 하는 코드가 추상화가 필요한지에 대해 더 생각해보도록 해야겠다
캬캭
종종 다른 디자인 시스템 라이브러리들을 보면 <Heading h1>
처럼 사용하는걸 보고 도대체 어떻게 저게 구현되었을까?
나도 저렇게 깔끔하게 props를 넘겨줘볼까? 하는 생각이 들어서 이번에 경험삼아 만들어봤다.
처음에는 geist ui
깃허브 [2] 를 참고하여 만들어보려했는데 이게 웬거 생각보다 로직이 너무 복잡하더라
그래서 주먹 구구식으로 간단하게 만들어봤다.
import React from "react";
type TextTag = "p" | "span" | "em" | "strong" | "i";
type TextType = "default" | "secondary";
type TextTagRecord = Partial<Record<TextTag, boolean>>;
const getStyleOfText = (type: TextType) => {
const styleMap: Record<TextType, string> = {
default: "text-base",
secondary: "text-text-secondary text-[0.8rem]",
};
return styleMap[type];
};
interface TextProps extends TextTagRecord {
children: React.ReactNode;
className?: string;
type?: TextType;
}
export const Text = ({
children,
className = "",
type = "default",
...tag
}: TextProps) => {
const textTag = Object.keys(tag).find((key) =>
["p", "span", "em", "strong", "i"].includes(key)
) as TextTag;
return React.createElement(
textTag,
{ className: `${getStyleOfText(type)} ${className}` },
children
);
};
디자인 시스템이라 하기에도 부족하긴 하지만 나중에 디자인 시스템을 만든다면 이런 식으로 구현 할 수 있지 않을까? 싶다.
물론 휴먼에러를 방지하기 위해 tag
props가 무조건 존재하도록 타입을 지정하거나 기본값을 지정해주는 식으로 발전시켜야겠지만