불필요한 컴포넌트 추상화에 대해 고민한 경험

ChoiYongHyeun·1일 전
1

리액트

목록 보기
31/31
post-thumbnail

우선 기능 개발부터 하고 컴포넌트에 대한 고민은 나중에 하자란 생각으로 사이드 프로젝트를 진행하던 중

구독중이던 매일매일 사이트에서 오늘의 질문으로 컴포넌트에 대한 질문을 발견했다.

이후 예전 리액트를 처음 시작했을 때 컴포넌트란 무엇일까? 하고 봤던 유튜브를 보며 커피를 먹던 중

내가 만들었던 매우 간단한 컴포넌트에도 적용 할 수 있을 거 같아서 부랴부랴 작업해봤다.

참고한 유튜브

토스ㅣ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 로 넘겨주는 형태이다.

영상에서 말하는 좋은 컴포넌트의 기준 중 위 컴포넌트에게 해당되는 부분을 체크해보았다.

  • 컴포넌트가 수정 및 확장에 용이한가?
  • 컴포넌트는 도메인과 상관 없이 표준 인터페이스에 가까운가?

저 방식은 수정 및 확장에 유연하게 대처 할 수 있는가?

이런 수정 사항이 존재한다고 해보자

만약 사용처에서 () 에 들어갈 문구의 색을 변경하고 싶다면? () 글자가 제목으로부터 마진값을 얼마나 더 주고 싶다면?

두 가지 방법이 있을 거 같다.

  1. 수정 사항을 반영한 새로운 컴포넌트 Reference ... Title2 컴포넌트 생성
  2. 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가 무조건 존재하도록 타입을 지정하거나 기본값을 지정해주는 식으로 발전시켜야겠지만

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글