ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] 리액트에서 컴포넌트 추상화에 대한 고민
    React 2024. 9. 21. 18:54

    추상화는 불필요한 세부 사항을 제거하고 중요한 부분만 남겨

    시스템을 더 쉽게 다룰 수 있게 만드는 과정입니다.

     

    프로그래밍에서는 인터페이스, 추상 클래스, 함수, 모듈 등을 통해 기능을 추상화할 수 있습니다.

    이를 통해 코드 재사용성, 유지보수성, 확장성을 높일 수 있습니다.

     

    React에서 컴포넌트를 설계할 때 이런 추성화 기법을 사용하여

    재사용성, 유지보수성, 유연성 등을 높일 수 있습니다.

     

    최근 리액트로 만들어진 다른 사람의 코드를 보던 중,

    문득 "이 부분을 좀 더 추상화하면 관리가 더 쉬워질까?"라는 생각이 들었습니다.

     

    코드를 처음 보면 간단해 보이지만,

    새로운 요구사항이 추가될 때마다 코드가 복잡해질 수 있지 않을까 고민했습니다.

    | 구현해 볼 컴포넌트

    제가 고민한 내용을 간단한 컴포넌트를 만들면서 공유해보려 합니다.

    아래 이미지와 같은 간단한 리스트 컴포넌트를 구현해 보겠습니다.

    해당 컴포넌트는 왼쪽 부분, 타이틀, 서브타이틀, 오른쪽 부분, 화살표를 통해

    재사용 가능하도록 만들어 보려 합니다.

     

    아래 예시로 구현하는 코드는 리액트, emotion을 사용하여 작성하였고,

    코드보다는 어떤 방식으로 컴포넌트를 추상화하려 했는지를 중점으로 봐주시면 좋을 거 같습니다.

    | 첫 번째 방법: 단일 컴포넌트에 여러 섹션 포함

    첫 번째 방법은 저에게 추상화에 대해 다시 생각해보게 한 코드입니다.

    하나의 컴포넌트 내부에 모든 섹션을 포함하여 관리하는 방식입니다.

     

    이 방식에서는 컴포넌트 내에 각각의 섹션(좌측, 내용, 우측)을 props로 전달하고,

    해당 컴포넌트 내에서 직접 섹션을 배치하고 렌더링 합니다. 

    구현

    우선 필요한 다른 파일들을 가져온 후 스타일을 지정해 주겠습니다.

    import { css } from "@emotion/react";
    import Flex from "../flex";
    import Text from "../text";
    
    interface Props {
      left?: React.ReactNode;
      contents: React.ReactNode;
      right?: React.ReactNode;
      withArrow?: boolean;
      onClick?: VoidFunction;
    }
    
    const listRowContainerStyles = css`
      padding: 8px 24px;
    `;
    
    const listRowLeftStyles = css`
      margin-right: 14px;
    `;
    
    const listRowContentsStyles = css`
      flex: 1;
    `;

    위에서 가져온 Flex 컴포넌트는 간단하게 flex 구조를 제공하는 컴포넌트이고,

    Text 컴포넌트글자의 스타일을 간단하게 제공하는 컴포넌트입니다.

     

    다음으로 해당 스타일로 리스트 컴포넌트를 만들어 줍니다.

    const ListRow = ({ left, contents, right, withArrow, onClick }: Props) => {
      return (
        <Flex as="li" css={listRowContainerStyles} onClick={onClick} align="center">
          <Flex css={listRowLeftStyles}>{left}</Flex>
          <Flex css={listRowContentsStyles}>{contents}</Flex>
          <Flex>{right}</Flex>
          {withArrow && <IconArrowRight />}
        </Flex>
      );
    };
    
    const IconArrowRight = () => {
      return (
        <svg>
          ...
        </svg>
      );
    };

    ListRow라는 컴포넌트를 만들고, 해당 컴포넌트에 왼쪽영역,

    가운데 내용 영역, 오른쪽 영역과 화살표를 포함시켜 줍니다.

     

    그다음

    const ListRowTexts = ({
      title,
      subTitle,
    }: {
      title: string;
      subTitle: string;
    }) => {
      return (
        <Flex direction="column">
          <Text fontWeight="bold">{title}</Text>
          <Text typography="t7">{subTitle}</Text>
        </Flex>
      );
    };
    
    ListRow.Text = ListRowTexts;
    
    export default ListRow;

    해당 리스트에 내용에 타이틀과 서브 타이틀을 정해진 스타일로 제공하기 위해

    ListRowTexts라는 컴포넌트를 만들어주고

    ListRow의 Text에 해당 컴포넌트를 포함시켜 준 후

    ListRow를 export default로 내보내줍니다.

     

    이제 해당 컴포넌트를 사용할 파일에서

    <ListRow
      withArrow
      left={<div>left</div>}
      contents={<ListRow.Text title="타이틀" subTitle="서브타이틀" />}
      right={<div>right</div>}
    />

    이런 식으로 사용해 주면 완성입니다.

    장단점

    이 방법으로 구현했을 때 제가 생각한 장단점은 다음과 같았습니다.

     

    우선 장점으로는

    • 컴포넌트가 단일 구조로 되어 있어 읽기 쉬웠고, 사용이 간편했습니다.
    • 모든 섹션이 한 곳에 모여 있어 컴포넌트의 구조를 한눈에 파악할 수 있었습니다.

    다음으로 단점으로는

    • 개별 섹션을 세부적으로 조정하거나 독립적으로 관리하기 어려웠습니다.
    • 새로운 기능이나 요구사항이 추가될 시 컴포넌트가 복잡해지고, 수정이 어려워질 우려가 있었습니다.

    | 두 번째 방법: 컴포넌트 분리

    두 번째 방법은 첫 번째 방법에서 단점을 보완하고자 고민해 본 방식입니다.

    이 방법은 컴포넌트를 좀 더 깊게 추상화하며, 각 섹션을 독립적인 컴포넌트로 분리하는 구조입니다.

     

    이렇게 하면 각 부분을 개별적으로 관리할 수 있고,

    필요한 경우 독립적으로 재사용할 수 있다 생각했습니다.

    구현

    우선 필요한 다른 파일들을 가져온 후 스타일을 지정해 주겠습니다.

    import { css } from "@emotion/react";
    import Flex from "./flex";
    import Text from "./text";
    
    interface Props {
      children: React.ReactNode;
      withArrow?: boolean;
      onClick?: VoidFunction;
    }
    
    const listRowContainerStyles = css`
      padding: 8px 24px;
    `;
    
    const listRowLeftStyles = css`
      margin-right: 14px;
    `;
    
    const listRowContentsStyles = css`
      flex: 1;
    `;

    다음으로 해당 스타일로 리스트 컴포넌트를 만들어 줍니다.

    export const ListRow = ({ children, withArrow, onClick }: Props) => {
      return (
        <Flex as="li" css={listRowContainerStyles} onClick={onClick} align="center">
          {children}
          {withArrow && <IconArrowRight />}
        </Flex>
      );
    };
    
    const IconArrowRight = () => {
      return (
        <svg>
          ...
        </svg>
      );
    };

    이번에는 ListRow 컴포넌트를 만들고, 해당 컴포넌트에 여러 섹션을 포함하지 않고,

    children을 그대로 받아서 렌더링 하도록 만들어 줍니다.

     

    그리고 리스트의 왼쪽 부분, 내용 부분, 오른쪽 부분을 각각 독립된 컴포넌트로 만들어 줍니다.

    export const ListLeft = ({ children }: { children: React.ReactNode }) => {
      return <Flex css={listRowLeftStyles}>{children}</Flex>;
    };
    
    export const ListContents = ({
      children,
      title,
      subTitle,
    }: {
      children?: React.ReactNode;
      title?: string;
      subTitle?: string;
    }) => {
      return (
        <Flex css={listRowContentsStyles}>
          {children}
          <Flex direction="column">
            <Text fontWeight="bold">{title}</Text>
            <Text typography="t7">{subTitle}</Text>
          </Flex>
        </Flex>
      );
    };
    
    export const ListRight = ({ children }: { children: React.ReactNode }) => {
      return <Flex>{children}</Flex>;
    };

     

    이제 해당 컴포넌트를 사용할 파일에서

    <ListRow withArrow>
      <ListLeft>left</ListLeft>
      <ListContents title="타이틀" subTitle="서브타이틀" />
      <ListRight>right</ListRight>
    </ListRow>

    이런 식으로 사용해 주면 완성입니다.

     

    이 컴포넌트는 ListRow가 전체 레이아웃을 담당하고,

    각 섹션은 개별적으로 컴포넌트로 분리됩니다.

    장단점

    첫 번째 방법의 단점을 보완하고자 이 방법으로 구현해 봤는데

    역시 장단점이 존재했습니다.

     

    우선 장점으로는

    • 각 섹션을 독립적으로 관리할 수 있어 수정 및 확장이 용이하다고 생각했습니다.
    • 특정 섹션을 여러 곳에서 재사용할 수 있어 프로젝트가 커지면 코드 중복을 줄일 수 있다 생각했습니다.
    • 새로운 요구사항이 추가되어도 기존 구조를 쉽게 확장할 수 있습니다.

    다음으로 단점으로는

    • 여러 컴포넌트로 분리되면서 코드가 길어지고 관리가 다소 복잡해질 우려가 있었습니다.
    • 컴포넌트를 조합해서 사용해야 하므로 좀 더 구체적인 사용법에 대한 설명(문서화)이 필요할 수 있습니다.
      (이 부분이 가장 중요하다고 생각)

    | 결론

    컴포넌트를 추상화할 때 간결함유연성 사이의 균형을 맞추는 것은 매우 중요하지만,

    실제로는 쉽지 않은 작업입니다.

    위 예시 코드는 매우 간단한 리스트 컴포넌트를 구현한 것이며,

    이를 통해 두 가지 방법을 비교해 보았습니다.

     

    단순한 코드의 경우 첫 번째 방식이 간결하고 빠르며 코드 작성이 수월합니다.

    하지만, 프로젝트가 복잡해지고 요구사항이 늘어날 가능성을 고려한다면,

    두 번째 방식처럼 각 섹션을 독립적으로 관리하는 방법도 좋은 선택일 수 있습니다.

     

    또한, 코드의 추상화는 프로젝트의 성격팀의 개발 스타일에 따라 달라질 수 있습니다.

    단순함을 우선할지, 유연성을 추구할지 고민해 보고,

    그에 맞는 방법을 선택하는 것이 중요하다고 생각합니다.

     

    사실, 저 역시 개발 경험이 많지 않아 어떤 방법이 정답인지는 잘 모르겠습니다.

    다만, 이런 고민을 통해 앞으로 프로젝트의 특성에 맞게

    더 나은 컴포넌트를 작성할 수 있는 역량을 키울 수 있을 거라 생각합니다.

Designed by Tistory.