컴포넌트, 레고처럼 쌓아보자! 컴포넌트 추상화의 모든 것

진우·2024년 9월 29일
4

Tech Tools

목록 보기
3/5
post-thumbnail

읽기 전에

본 글은 컴포넌트 추상화 개념을 처음 접하는 프론트엔드 개발자를 위한 글입니다. 꼭 알아야 할 핵심 개념과 실제 프로젝트에서 유용하게 사용할 수 있는 방법들을 중심으로 설명하였으니 참고 바랍니다.

"React": "^18"

컴포넌트란?

리액트는 컴포넌트 기반의 구조를 가지고 있기 때문에 컴포넌트의 개념은 매우 중요합니다. 컴포넌트는 부품, 요소라는 의미를 가지고 있는데 이것을 웹 개발 측면에서 본다면 전체 시스템을 구성하는 UI요소 라고 할 수 있습니다.


이 페이지는 애플 공식스토어 웹 페이지입니다. 여기에는 다양한 컴포넌트 들이 있는데요


그 중에서도 A와 B로 표시해놓은 곳을 컴포넌트로 볼 수 있습니다. 이 컴포넌트들을 반복적으로 사용해서 하나의 웹 페이지를 보여주고 있습니다.

컴포넌트 추상화란?

컴포넌트 추상화는 여러번 반복되는 유사한 기능들을 하나의 일반화된 형태로 만들어 재사용성을 높이는 방법입니다. 쉽게 말해 다양한 형태로 존재하는 여러개의 기능들을 하나의 공통된 구조로 만들어 필요할 때마다 불러와 사용할 수 있게 해주는 기법입니다.

그럼 다양한 형태는 어떤 형태일까요?


이건 쿠키의 틀로 여러 형태의 쿠키를 만들어본 예시입니다. 하나의 쿠키 틀을 사용하면 다양한 모양의 쿠키를 만들 수 있듯이, 컴포넌트도 하나의 공통된 구조(코드)를 통해 여러 기능을 구현할 수 있습니다. 쿠키를 만들 때는 같은 틀을 사용하더라도, 반죽, 설탕, 토핑 등 다양한 재료를 더해 여러 가지 맛과 모양을 낼 수 있죠.

컴포넌트 추상화도 이와 유사합니다. 다양한 기능을 가진 컴포넌트들이 있지만, 이들이 사용하는 재료는 props와 같은 입력값일 뿐, 그 본질은 공통된 코드로 이루어져 있습니다. 즉, 쿠키의 틀처럼 하나의 컴포넌트 구조를 기반으로 여러 형태의 컴포넌트를 만들 수 있는 것이죠.

그럼 컴포넌트 추상화를 왜 해야할까요?

1.코드 재사용성
추상화를 통해 동일한 기능을 여러 번 작성할 필요 없이, 한 번 작성한 코드를 여러 곳에서 재사용할 수 있습니다. 이를 통해 코드 중복을 줄이고, 개발 속도를 높일 수 있습니다.

2.유지보수성 향상
코드 수정이 필요할 때, 한 곳만 수정하면 모든 곳에 적용되기 때문에 유지보수가 훨씬 간편해집니다. 중복된 코드가 없다면, 오류를 수정하거나 새로운 기능을 추가할 때 발생하는 실수를 줄일 수 있습니다.

3.테스트 용이성
추상화된 컴포넌트는 독립적으로 테스트할 수 있습니다. 이렇게 하면 각 컴포넌트의 테스트 범위가 명확해지고, 추적이 쉬워집니다. 반면, 추상화되지 않은 코드에서는 전체 코드를 테스트해야 하므로, 문제 발생 시 어떤 부분에서 오류가 나는지 파악하기 어려울 수 있습니다.

4.코드 가독성 향상
추상화된 코드는 모듈화되어 있어 읽기 쉽고, 이해하기도 쉬워집니다. 각 컴포넌트가 명확한 책임을 갖기 때문에, 코드 구조가 더 직관적이고 유지보수가 용이해집니다.

결론적으로, 컴포넌트 추상화를 통해 코드의 재사용성을 높이고 유지보수성을 강화할 수 있으며, 이를 통해 작업 효율을 높여 개발 속도를 빠르게 할 수 있습니다.

컴포넌트 추상화 원칙

컴포넌트 추상화를 할 때에도 효율적으로 추상화를 하기 위해 몇 가지 권장되는 원칙이 있습니다.

1. 단일 책임 원칙 (Single Responsibility Principle)
각 컴포넌트는 하나의 책임만 가져야 합니다. 즉, 하나의 기능만을 담당해야 합니다. 예를 들어, 한 컴포넌트에 두 개의 버튼이 있다고 가정해보겠습니다. 만약 두 버튼 중 하나만 사용하거나 수정하고 싶을 때, 이 컴포넌트가 두 버튼을 모두 포함하고 있다면 그 컴포넌트를 효율적으로 사용할 수 없습니다. 결국, 새로운 컴포넌트를 만들어야 하므로, 추상화된 컴포넌트가 비효율적이게 됩니다. 따라서 각 컴포넌트는 하나의 책임만을 가져야 재사용성과 유지보수성이 높아집니다.

2. 높은 응집도와 낮은 결합도 (High Cohesion, Low Coupling)
컴포넌트 내부의 로직은 긴밀하게 작성되어야 하지만, 다른 컴포넌트와는 느슨하게 결합되어야 합니다. 내부 로직이 명확하고 체계적으로 구성되어 있어야 해당 컴포넌트를 쉽게 재사용할 수 있기 때문입니다. 반면, 컴포넌트가 다른 컴포넌트와 너무 밀접하게 결합되어 있으면 하나의 컴포넌트를 수정할 때 다른 컴포넌트에 영향을 미칠 수 있어 유지보수가 어려워집니다.

3.명확한 인터페이스 정의
컴포넌트 간의 상호작용은 명확하게 정의되어야 합니다. 즉, 어떤 컴포넌트가 어떤 데이터를 주고받는지 쉽게 알 수 있어야 합니다. 만약 데이터 흐름이 명확하지 않다면, 컴포넌트를 이해하기 어렵고, 재사용하는 데 불편함이 생깁니다. 명확한 인터페이스는 코드의 가독성을 높이고, 유지보수를 쉽게 만듭니다.

컴포넌트 추상화 단계

컴포넌트 추상화는 몇 가지 단계를 통해 이루어집니다:

1.중복 코드 식별
먼저, 기존 코드에서 중복된 코드를 식별하거나, 코드를 작성하기 전에 중복될 가능성이 있는 코드를 미리 예상합니다. 코드 작성 전부터 중복되는 부분을 따로 분리해 설계하는 것이 중요합니다.

2.공통 패턴 추출
중복된 코드에는 반드시 공통된 패턴이 있을 것입니다. 이 공통된 부분을 추출하여 컴포넌트화합니다. 만약 공통된 부분이 하드코딩되어 있다면, 상태 관리를 통해 동적으로 변경할 수 있도록 코드를 수정해야 합니다.

3.컴포넌트 분리 및 재구성
공통된 패턴을 추출한 후, 컴포넌트를 적절하게 분리하고, 애플리케이션 구조에 맞게 재구성합니다. 이렇게 하면 코드가 더 효율적이고 유지보수하기 쉬운 구조가 됩니다.

이 단계를 예시를 통해 좀 더 구체적으로 설명드리겠습니다.

import React, { useState } from 'react';

const Tabs = () => {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div>
      <div>
        <button onClick={() => setActiveTab(0)}>Tab 1</button>
        <button onClick={() => setActiveTab(1)}>Tab 2</button>
      </div>
      <div>
        {activeTab === 0 && <div>Content for Tab 1</div>}
        {activeTab === 1 && <div>Content for Tab 2</div>}
      </div>
    </div>
  );
};

const App = () => (
  <div>
    <h1>My App</h1>
    <Tabs />
  </div>
);

export default App;

이 코드는 간단한 탭 기능을 구현한 예제입니다. 하지만 컴포넌트를 추상화하지 않고 단순히 컴포넌트 분리만 해 놓은 상태이기 때문에 몇 가지 문제가 발생할 수 있습니다. 우선, 탭 버튼과 콘텐츠가 하드코딩되어 있어, 탭 수가 늘어나거나 내용을 변경해야 할 경우 매번 코드를 수정해야 합니다. 새로운 요구사항이 생길 때마다 수정할 부분도 많아지겠죠. 이런 방식은 확장성도 낮고 유지보수도 어렵습니다. 추상화하지 않으면, 이런 문제는 계속 반복될 것입니다.

그래서 이 동일한 기능을 컴포넌트 추상화를 통해 기능을 구현해 봤습니다. 컴포넌트 추상화를 하는 방법은 여러가지가 있는데요 고차 컴포넌트 방식, 렌더 프롭스 방식, 커스텀 훅을 이용한 방식, 컨텍스트를 이용한 방법등 여러 방법이 있습니다. 이번 예시에서는 Context API을 사용하여 컴포넌트를 추상화해 보았습니다.

import React, { useState, createContext, useContext } from 'react';

// TabsContext 생성: 탭 상태를 관리하기 위한 컨텍스트
const TabsContext = createContext();

// Tabs 컴포넌트: 탭 상태 관리 및 하위 컴포넌트에 상태 제공
const Tabs = ({ children }) => {
  const [activeTab, setActiveTab] = useState(0); // 현재 활성화된 탭 상태
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div>{children}</div> {/* 하위 컴포넌트들 렌더링 */}
    </TabsContext.Provider>
  );
};

// TabList 컴포넌트: 탭 버튼 리스트를 감싸는 컴포넌트
const TabList = ({ children }) => {
  return <div>{children}</div>;
};

// Tab 컴포넌트: 개별 탭 버튼, 클릭 시 탭을 변경
const Tab = ({ index, children }) => {
  const { activeTab, setActiveTab } = useContext(TabsContext); // 현재 활성 탭과 탭 변경 함수 사용
  return (
    <button 
      onClick={() => setActiveTab(index)} 
      style={{ fontWeight: activeTab === index ? 'bold' : 'normal' }}
    >
      {children}
    </button>
  );
};

// TabPanels 컴포넌트: 탭의 콘텐츠를 감싸는 컴포넌트
const TabPanels = ({ children }) => {
  const { activeTab } = useContext(TabsContext); // 현재 활성화된 탭 상태 사용

  // children이 배열로 들어온다고 가정하고, activeTab에 맞는 콘텐츠만 렌더링
  return <div>{children[activeTab]}</div>; // 활성화된 탭에 맞는 콘텐츠만 렌더링
};

// TabPanel 컴포넌트: 개별 탭의 콘텐츠
const TabPanel = ({ children }) => {
  return <div>{children}</div>;
};

// App 컴포넌트: Tabs, TabList, Tab, TabPanels, TabPanel을 사용해 탭 구조 정의
const App = () => (
  <div>
    <h1>My App</h1>
    <Tabs>
      <TabList>
        <Tab index={0}>Tab 1</Tab>
        <Tab index={1}>Tab 2</Tab>
      </TabList>
      <TabPanels>
        <TabPanel>Content for Tab 1</TabPanel>
        <TabPanel>Content for Tab 2</TabPanel>
      </TabPanels>
    </Tabs>
  </div>
);

export default App;

1. TabsContext 생성
먼저 createContext 함수를 사용하여 TabsContext를 생성했습니다. 이 컨텍스트는 탭의 상태와 탭을 변경하는 함수 (setActiveTab)를 공유하기 위한 목적으로 사용됩니다.

2. Tabs 컴포넌트
Tabs 컴포넌트는 탭의 상태를 관리하며, TabsContext.Provider를 통해 상태를 하위 컴포넌트들에게 전달합니다. 탭을 관리하는 상태는 활성화된 탭과 탭을 변경하는 함수입니다. 이 컴포넌트는 트리의 상위에 위치하여 자식 컴포넌트들에 상태를 공유합니다.

3. TabList 및 Tab 컴포넌트
TabList는 탭 버튼들을 감싸는 역할을 하고, Tab 컴포넌트는 각 탭 버튼을 정의합니다. Tab 컴포넌트는 useContext 훅을 사용하여 TabsContext의 상태를 가져오고, 현재 활성화된 탭과 클릭 시 탭을 변경하는 동작을 처리합니다.

4. TabPanels 및 TabPanel 컴포넌트
TabPanels 컴포넌트는 여러 탭의 콘텐츠를 감싸며, 현재 활성화된 탭에 맞는 콘텐츠만 렌더링합니다. 각 TabPanel 컴포넌트는 개별 탭의 내용을 정의합니다.

이 구조의 장점은 재사용성유연성입니다. 추상화를 통해 각 컴포넌트를 자유롭게 커스터마이징할 수 있습니다. 예를 들어, TabList나 TabPanel에 새로운 탭이나 콘텐츠를 쉽게 추가할 수 있습니다. 또한, Tab 컴포넌트를 통해 탭의 개수나 순서도 자유롭게 변경할 수 있습니다.

이와 달리, 추상화되지 않은 컴포넌트는 하드코딩된 구조로 인해 유연성이 떨어지고, 탭의 수나 내용을 변경할 때마다 코드를 수정해야 합니다. 하지만 컴포넌트 추상화를 적용하면, 자식 컴포넌트들을 원하는 대로 커스터마이징할 수 있어 유지보수와 확장성이 높아집니다.

결론

컴포넌트 추상화코드의 재사용성과 유지보수성을 높이는 데 매우 유용합니다. 추상화를 통해 중복을 줄이고, 더 효율적으로 코드를 작성할 수 있기 때문입니다.

하지만, 무분별하고 과도한 추상화는 오히려 문제가 될 수 있습니다.

컴포넌트를 추상화할 때, 예시를 봤듯이 코드가 복잡해지면서 코드 길이가 늘어나고, 구조가 복잡해질 수 있습니다. 특히 팀과 협업할 때는 이러한 복잡성이 다른 팀원들에게 이해하기 어려운 코드가 될 수 있습니다. 또한, 추상화된 컴포넌트는 개발 시간이 길어질 수 있으며, 너무 간단한 컴포넌트에 적용할 경우 개발 시간이 증가하는 부작용이 생길 수 있습니다.

따라서, 컴포넌트 추상화적절한 수준에서 적용하는 것이 중요하다고 생각합니다. 필요할 때는 구체적인 컴포넌트를 사용하는 것이 더 나은 선택일 수 있습니다. 결국, 효율성과 복잡성의 균형을 맞추는 것이 컴포넌트 추상화의 핵심입니다.

항상 블로그를 작성하며 드는 생각이지만 세상에 완벽한건 없다고 생각합니다. 기술은 도구일 뿐 도구는 프로젝트의 효율성과 생산성을 높이기 위한 수단이지 절대적인 정답은 없다고 생각합니다.

틀린 내용 또는 읽기 불편하신 점이 있다면 언제든지 지적해 주시면 감사하겠습니다. 긴 글 읽어주셔서 감사합니다!

Reference

https://www.youtube.com/watch?v=6YZhSvRqddw&t=89s&ab_channel=ZeroChoTV
https://www.youtube.com/watch?v=aAs36UeLnTg&t=668s&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC

profile
Frontend 개발이 재밌어요 ><

0개의 댓글

관련 채용 정보