[React Design Pattern] 변경에 유연한 Picker Component 만들기

Quartz 쿼츠·2022년 12월 12일
1
post-thumbnail

#0 Introduction

리액트를 사용하여 프론트엔드 개발을 하다보면 유사한 로직을 가지는 코드를 반복적으로 작성하는 경우가 자주 발생한다. 첫 번째는 같은 역할을 하는 컴포넌트를 다시 개발하는 경우이고, 두 번째는 디자인이나 서비스의 변경이 생겨 코드를 변경하는 경우이다.

즉, 서비스가 확장되고 제품이 변경됨에 따라 우리는 코드를 반복적으로 작성하게 된다.

<Post Black Belt> 프로젝트에서 다양한 UI를 개발하면서 이러한 문제점을 개선하기 위해 변경에 유연한 컴포넌트를 만드는 디자인 패턴을 공부하게 되었다. 이 글에서는 나에게 좋은 인사이트를 주었던 토스의 한재엽 개발자님의 컨퍼런스 강연(Effective Component 지속 가능한 성장과 컴포넌트)을 소개하고 해당 내용을 적용한 나의 사례를 이야기할 예정이다. 우리가 만드는 제품(ex. 웹 서비스)은 사용자를 만족시키기 위해 변화하며 성장한다. User Interface를 담당하는 프론트엔드 개발은 이러한 변화에 맞닿아있으며 개발자들은 각자의 기준에 맞춰 컴포넌트를 분리하여 변경에 유연하게 개발하는 환경을 만든다.

#1 Effective Component 지속 가능한 성장과 컴포넌트 (강연)

#1.1 컴포넌트를 분리하는 기준 세우기

해당 강연에서 제시한 컴포넌트를 분리하는 기준은 다음과 같다.

  1. Headless 기반의 추상화
    디자인에 의존하는 UI데이터를 관리하는 로직을 각각 추상화하기
  2. Composition: 한 가지 역할만 하는 컴포턴트 혹은 컴포넌트들의 조합으로 구성
    독립적인 각 컴포넌트를 합성하여 재사용하기
  3. 도메인(비즈니스 로직)을 분리
    컴포넌트는 기존에 알고있는 표준(도메인을 포함하지 않는) 내용일수록 이해하기 쉬움

Headless UI component란 기능은 있지만 스타일이 없는 컴포넌트를 말한다. 디자인과 데이터를 각각 추상화(ex custom hooks 생성)하여 컴포넌트가 한 가지 역할에만 집중하도록 만든다. 이는 우테코 프리코스를 수강하며 공부하였던 UI 아키텍쳐의 기본인 MVC, MVP 패턴의 해결 방법과 유사하다.

사용자에게 시간과 음식 입력을 받는 서비스를 만든다고 하자. 우리는 각 컴포넌트를 TimePickerFoodPicker로 나누어 개발할 수도 있지만, Picker라는 표준 컴포넌트를 기반으로 컴포넌트를 합성하여 반복되는 로직을 개선할 수 있다. 이러한 방식으로 개발할 때 비즈니스(데이터) 로직과 UI 로직 중 어떤 것을 스스로 처리하고 어떤 것을 위임할지 잘 고민해보아야 한다. 이를 바탕으로 내가 느낀 컴포넌트 분리의 핵심을 아래의 한 문장으로 정리해보았다.

컴포넌트를 잘게 쪼개서 모든 사람이 알고 있는 기능을 하도록 만들자!

#1.2 코드를 짜는 방법

위 내용을 바탕으로 더 수월하게 개발하기 위해 해당 강연에서 제시한 개발 방법론은 아래와 같다.

  1. Component Interface를 먼저 고민하기
    도메인을 포함하지 않는 표준 컴포넌트가 이미 존재한다고 가정하고 Component Interface를 작성한다. 어떤 데이터를 props로 공유해야 할지 컴포넌트의 의도, 기능, 표현을 먼저 파악한다.
  2. Component를 나누는 이유 생각하기
    현재 컴포넌트를 분리하는 이유가 복잡도를 낮추는 방향인지 혹은 재사용 가능한 컴포넌트를 만들기 위함인지 파악하여야 한다.

#2 지속가능한 List Picker Component 만들기 (사례)

위 내용을 바탕으로 실제 프로젝트 코드를 개선한 사례를 소개한다. 현재 내가 개발중인 서비스는 주짓수 수련자를 위한 일기 기록 앱이다. React Native를 사용하고 있으므로 React와 상이한 JSX 코드가 등장하는 것을 참고하길 바란다. 아래와 같이 DiaryCategoryPickerTechCategoryPicker라는 두 개의 컴포넌트를 개발하고 보니 디자인 외에는 유사한 동작을 하고 있음을 깨달았다. 다음과 같이 반복되는 사항들을 개선하여 컴포넌트의 재사용성을 높인 경험에 대해 공유해보려 한다.

  • 디자인의 반복: 카테고리를 리스트 형태로 나열 & 사용자의 선택 외 항목들은 opacity 낮춤
  • 데이터 처리의 반복: 사용자가 선택한 항목을 전역 상태 관리

#2.1 Component Interface 고민하기

따로 개발된 두 개의 컴포넌트를 하나의 ListPicker로부터 상속시키기 위하여 아래와 같은 Component Interface를 먼저 구상하였다. ListPicker 에서 다뤄야할 데이터와 관련된 로직들은 item dispatch getCurrIndex이며 UI와 관련된 로직은 jsx props로 전달할 예정이다.

// DiaryCatListPicker
<ListPicker
  items={DIARY_CAT}
  dispatch={dispatchPressedCategory}
  getCurrIndex={getCurrIndex}
  jsx={listComponent}
/>

// TechCatListPicker
<ListPicker
  items={TECH_CAT}
  dispatch={dispatchPressedCategory}
  getCurrIndex={getCurrIndex}
  jsx={listComponent}
  />

#2.2 도메인 분리하기


그 전에 특정 비즈니스가 아닌 표준 컴포넌트 ListPicker에 대해 먼저 코드를 작성해보자. 임의의 데이터와 UI로 아래와 같은 반복성을 ListPicker 컴포넌트에 위임하였다.

  • 디자인의 반복: 카테고리를 리스트 형태로 나열 & 사용자의 선택 외 항목들은 opacity 낮춤
  • 데이터 처리의 반복: 사용자가 선택한 항목을 처리
const ICON_OPACITY = {
  INACTIVE: 0.3,
  ACTIVE: 1,
};

const items = [0, 0, 0, 0, 0, 0]; // 임의의 데이터

export default function ListPicker() {
  const opacityArr = Array(items.length).fill(ICON_OPACITY.INACTIVE);
  const [iconsOpacity, setIconOpacity] = useState(opacityArr);
  const storeDiary = useSelector((state) => state.editDiary);

  const updateActiveIconOpacity = (i) => {
    setIconOpacity(() => {
      const result = opacityArr;
      result[i] = ICON_OPACITY.ACTIVE;
      return result;
    });
  };
  const handleOnPress = (CAT, i) => {
    updateActiveIconOpacity(i);
    // 데이터 처리 로직 작성
  };
  
  return (
    <>
      {items.map((CAT, i) => {
        return (
          <Pressable
            key={CAT.ID}
            style={{ opacity: iconsOpacity[i] }}
            onPress={handleOnPress.bind(this, CAT, i)}
          >
            <Text>{CAT}</Text> // 임의의 UI
          </Pressable>
        );
      })}
    </>
  );
}

#2.3 데이터 로직 주입하기

이제 우리는 도메인이 분리된 ListPicker라는 표준 컴포넌트에 데이터와 UI 로직을 각각 주입하여 DiaryCategoryPickerTechCategoryPicker 컴포넌트의 비즈니스 로직을 구현할 수 있다. 먼저 데이터 로직을 주입해보자.

// DiaryCatListPicker
<ListPicker
  items={DIARY_CAT}
  dispatch={dispatchPressedCategory}
/>

// TechCatListPicker
<ListPicker
  items={TECH_CAT}
  dispatch={dispatchPressedCategory}
 />

items prop은 각 카테고리별 항목들을 전달하며, 사용자가 클릭한 항목을 전역으로 관리하기 위한 redux 관련 함수를 dispatch prop으로 전송한다. #2.2의 ListPicker 에 아래와 같은 데이터 로직을 추가하면 디자인은 존재하지 않고 데이터만 처리하는 Headless UI 컴포넌트를 작성할 수 있다.

export default function ListPicker({ items, dispatch }) {

  const handleOnPress = (CAT, i) => {
    updateActiveIconOpacity(i);
    dispatch(CAT.ID); // 데이터를 전역으로 저장
  };
  
  return (
    <>
      {items.map((CAT, i) => {
        return (
          <Pressable>
            <Text>{CAT}</Text> // 임의의 UI
          </Pressable>
        );
      })}
    </>
  );
}

#2.4 UI 로직 주입하기(컴포넌트 합성)


이제 각 카테고리 Picker가 가진 UI만 주입하면 컴포넌트 구현이 완료된다. UI 로직은 jsx prop으로 ListComponent를 전달하여 ListPicker에서 컴포넌트를 합성하는 방식을 사용하였다.

// DiaryCatListPicker
const DiaryListComponent = (CAT) => {
  return (
    <View style={styles.diaryCategory}>
      <Image style={styles.diaryCategoryImg} source={CAT.IMG_SRC} />
      <Text style={styles.diaryCategoryTitle}>{CAT.KOR}</Text>
    </View>
  );
};
...
<ListPicker
   jsx={DiaryListComponent}
/>


// TechCatListPicker
const TechListComponent = (CAT) => {
  return (
    <View style={styles.diaryCategory}>
      <Text style={styles.diaryCategoryEng}>{CAT.ENG}</Text>
      <Text style={styles.diaryCategoryKor}>{CAT.KOR}</Text>
    </View>
  );
};
...
<ListPicker
   jsx={TechListComponent}
 />

위와 같이 각 카테고리 Picker의 UI를 작성하고 #2.3에서 완성된 Headless UI component에 jsx를 아래와 같이 합성하면 UI 로직을 간단하게 주입할 수 있다.

export default function ListPicker({ jsx }) {

  return (
    <>
      {items.map((CAT, i) => {
        return (
          <Pressable>
            {jsx(CAT)} // UI 로직 합성
          </Pressable>
        );
      })}
    </>
  );
}

마치며

강연을 듣고 큰 감명을 받았지만 실제 나의 프로젝트에 적용하기까지는 기간이 꽤 걸린 것 같다. 규모가 작은 프로젝트를 개발하고 있기도 했고, 토스의 개발자분이 올려놓은 코드 구조를 그대로 따라가려 해서 어디에 사용할지 감이 오지 않은 점도 있었다. 시간이 지나면서 우테코 프리코스를 통해 간단한 예시로 데이터 처리 로직과 UI 로직 분리의 필요성을 직접 느끼고, 리팩토링을 배우면서 코드의 재사용성을 고민하다보니 강연을 실제 코드로 접목시킬 수 있었다. 당장은 적용하기 힘들더라도 다른 개발자 분들의 고민과 사유가 담겨있는 강연 영상을 자주 봐야겠다는 교훈을 얻게 되었다.

자세한 프로젝트 코드는 깃허브에서 확인할 수 있습니다.

참고자료

토스ㅣSLASH 22 - Effective Component 지속 가능한 성장과 컴포넌트

profile
Code what we love. 좋아하는 것들을 구현하고 있는 프론트엔드 개발자입니다. 사용자도 함께 만족하는 서비스를 만들고 싶습니다.

0개의 댓글