캘린더 만들면서 우여곡절 🥹 (Feat: Headless)

jeongshin·2023년 1월 21일
24
post-thumbnail

캘린더 뭐 별거 없드라~

대략 1년전 22년 2월에 입사 했을때 두번째로 진행한 feature 가
챌린지 인증이 가능한 기간을 사용자들에게 보여주는 캘린더 UI 개발 이였다.

이때까지만 해도 디자이너분들에게 절대 복종 🫡 이였기 때문에 (지금은 가끔 반항할때도 있음)

기능적 요구사항을 파악하거나 등의 과정을 거의 스킵한 채
빠르게 피그마만 보면서 UI만 똑같이 그려나가기 시작했다.

그래서 대략 1주일 공을 쏟아서 완성한 결과물 🥳

이때까지만 해도 잘 해냈다고 생각했고 만족스러운 결과물이 나왔다고 생각했다.

어라라?

그러다 시간이 대략 1년 정도 지나서 작년 12월 랜선대회 feature 를 진행하면서
캘린더 개발에 대한 요구사항이 또 들어왔다.

Product Designer: 제리 ~ 지난번에 캘린더 개발 했으니까 빠르게 끝낼 수 있죠?

Jerry: 어.. 아니요.. 그거 못 쓸거 같은데.. 거의 처음부터 다시 개발해야할 것 같아요.

이때 기존에 챌린지 캘린더를 개발할 때 문제점이 뭐였는지를 고민하기 시작했다.

  • 캘린더 하나의 날짜에 대한 데이터가 특정 도메인(챌린지) 에 의존적
  • 캘린더 하나의 날짜 스타일이 다른데 이를 수정 & 확장하기 어려운 구조 (핑크색 배경 등등)

대략 이런 문제점으로 코드 재활용성이 꽝이 되어 버렸기에 처음부터 모든걸 다시 개발 해야하는 상황이 발생했다.

Headless Component

Headless 에 대한 글을 종종 봤었는데 실제로 적용 하려니까 막상 어떤식으로 개발해야할지 감이 안왔었는데 좋은 예시는 가까운 곳에 있었다.

리액트 네이티브에서 FlatList 가 정말 최고의 컴포넌트라고 느끼고 대부분의 리스트를 FlatList 로 개발하고 있었는데

왜 이렇게 사용성이 좋을까? 를 고민 했을때 renderItem 이 최고의 장점이라고 생각이 들었다.

FlatList API 는 데이터를 어떻게 그릴지 renderItem 을 통해 스타일을 직접 정의할 수 있었고 data 와 구분 지어서

도메인 로직 (data) 와 UI (renderItem) 을 분리해서 개발할 수 있도록 유도했었고 여기서 힌트를 얻었다.

Calendar V2

그렇게 개발하게 된 캘린더 V2 API 를 소개하자면 대략 아래와 같다.

type CalendarProps<T> = {
  startDate: Date;
  endDate: Date;
  initialActiveDate?: Date;
  itemHeight: number;
  itemWidth?: number;
  bgColor?: ColorKeys;
  display: 'month' | 'week';
  CalendarHeaderComponent?: React.ReactElement;
  children?: React.ReactNode;
  onViewableMonthChanged?: (date: Date) => void;
} & Partial<FlatListProps<CalendarDateItem[]>> &
  Pick<CalendarRenderProps<T>, 'dayItemMapFn' | 'renderDayItem'>;

여기서 핵심 개념이 dayItemMapFnCalendarDateItem 에 대한 타입인데

우선 CalendarDateItem 이 하나의 날짜에 대한 최소한의 정보를 나타내는 데이터

type CalendarDateItem = {
  type: 'date' | 'placeholder'; // 해당 달에 포함된 날짜인지 ex) 2월 캘린더에 포함된 1월 31일은 'placeholder' 에 해당 되는 날짜
  date: Date; // 날짜
  weekIndex: number; // 주 단위로 봤을때 해당일의 index
  monthIndex: number; // 월 단위로 봤을때 해당일의 index
  isBetweenRange: boolean; // startDate ~ endDate 사이인지
  isToday: boolean; // 해당 날짜가 오늘인지
  isActiveDate?: boolean; // 현재 커서가 가있는 날짜인지
};

여기에 각각 도메인 로직, 데이터를 매핑해주는 함수를 열어주었고

function Calendar<T extends CalendarDateItem>({
  children,
  renderDayItem,
  dayItemMapFn,
  // ... 생략

코드 레벨 예시를 조금 더 들자면

// CalendarDateItem 을 확장하는 도메인 로직이 포함된 데이터를 매핑하는 함수
const dayItemMapFn = useCallback(
    (item: CalendarDateItem): MyCustomItemType => {
      return {
        ...item,
        backgroundImage: utils.filterImageByDate(images, item.date),
      };
    },
    [images],
);

// FlatList renderItem 에 해당되는 renderDayItem
const renderDayItem = useCallback((item: MyCustomItemType) => {
   return <MyCustomDayItem {...item} />;
}, []);

요런식으로 개발 해보았다.

결과물

이렇게 랜선대회용 캘린더가 완성이 되었다.

이제는 지난번과 같은 질문이 왔을때,

Product Designer: 제리 ~ 지난번에 캘린더 개발 했으니까 빠르게 끝낼 수 있죠?

Jerry: 어우 그럼유~. 지난번에 들었던 공수의 1/2 이면 되죠~

잘못된 내용 있으면 댓글 남겨주세요 ~ 🙇🏻‍♂️

profile
Jerry 의 개발 블로그 🙇🏻‍♂️

0개의 댓글