프로젝트/하루한냥

[트러블 슈팅] 11월이 되어야 관측되는 오류가 있다?

알파카털파카 2023. 11. 3. 20:47
[트러블 슈팅]
11월이 되어야 관측되는 오류가 있다?

 

 

어느덧 프로젝트 완성이 코 앞으로 다가왔다. 모바일에서도 예쁘게 보이는지 스타일링과 UIUX에 중점을 맞춰 리팩토링을 진행하고 있었다. 그러다보니 달력은 넘어가고, 2023년이 2달 밖에 남지 않은 시점이 되었다. 그런데 갑자기 캘린더 헤더가 이상하게 동작했다. 이전 날짜로 이동하는 것은 제대로 되는데, 다음 달로 넘어가질 않았다! 이게 무슨 소린가? 그동안 정상 동작하는걸 몇 달 간 봐왔는데? 알고보니 정말 11월이 되지 않았다면 발견하지 못했을 오류였다. 프로젝트 개발 기간이 길어진 것에 고마워해야 하는 것인지? 😂 만약 실제 서비스하는, 앱스토어에 올라간 앱이라면 급하게 오류 고치고 심사받느라 진땀 뺐을 것이다.  

 

 


 

 

캘린더 헤더 오류 발생

캘린더 헤더는 달력 페이지와, 타임라인 페이지 두 곳에서 사용된다. 공통으로 사용하는 컴포넌트이기 때문에, 물론 두 페이지 모두에서 동일한 오류가 발생한다. 과거로만 갈 수 있는 편도행 달력이다. 

 

캘린더 페이지, 타임라인 페이지

 

캘린더 헤더

 

오류 영상

 

 

 

 

1. 캘린더 헤더 코드 살펴보기

캘린더 헤더를 구현할 때 Zustand를 사용해 store를 생성했다. 관련 내용은 이전 블로그에 포스팅한 글이 있다. 그렇기 때문에 이번 포스트에는 이 로직에 대해 구구절절 작성하기보다는 오류가 발생한 포인트와 해결 방법을 남겨볼 것이다. 참고로 현재 프로젝트 코드는 이전 글 코드에서 몇 번 수정/보완된 부분이 있으므로 핵심 컨셉만 가져가도록 한다. 

 

 

[하루한냥] 캘린더 구현 : Zustand를 이용한 상태관리 구현하기

[하루한냥] 캘린더 구현 : Zustand를 이용한 상태관리 구현하기 이전 시간에 만든 캘린더 컴포넌트에 Zustand를 이용해 상태관리 store를 구현했다. 헤더에서 날짜가 바뀌면 캘린더 컴포넌트의 달력이

shinjungoh.tistory.com

 

// useDateStore

import { create } from 'zustand';

type State = {
  currentDate: Date;
  targetDate: Date | null;
};

type Action = {
  getFirstDayOfMonth: (date: Date) => Date;
  setTargetDate: (year: number, month: number) => void;
};

type DateStore = State & Action;

const INITIAL_DATE = new Date();

const useDateStore = create<DateStore>((set) => ({
  currentDate: INITIAL_DATE,
  targetDate: null,
  getFirstDayOfMonth: (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1),
  setTargetDate: (year: number, month: number) =>
    set({
      targetDate: new Date(year, month - 1),
    }),
}));

export default useDateStore;

 

// 날짜 변경 로직

const handleChangeTargetDate = (type: 'prev' | 'next') => {
  if (targetDate !== null) {
    const mappedTypeNumber = type === 'prev' ? -1 : +1;
    targetDate.setMonth(targetDate.getMonth() + mappedTypeNumber);
    const year = targetDate.getFullYear();
    const month = targetDate.getMonth() + 1;
    if (page === 'calendar') {
      navigate(`/calendar?year=${year}&month=${month}`);
    } else if (page === 'timeline') {
      navigate(`/timeline?year=${year}&month=${month}`);
    }
  }
};

 

고백하건대, 작성한지 좀 지난 코드는 내 손으로 짰어도 기억이 안 날 때가 있다.(사실 그런 코드가 대부분이다🥲) 블로그에 작성해놓은 것이 미래의 나(= 지금 현재의 나)에게 도움이 되고 있다. 이게 기록의 참맛이지. 이번 오류를 해결하는 과정에서 헷갈렸던 부분까지 이전 글에 다 풀어써놔서 다시금 이해가 되었다. store에서 setTargetDate의 month에 왜 -1을 해주는 것일까 고민했었는데 해당 부분의 코멘트가 있었다. 

 

setTargetDate에서 month에 -1을 해 준 이유는 다른 곳에서 month값을 사용할 때 0부터 시작하는 month의 반환값에 +1을 해서 사용했기 때문에, 다시 자바스크립트가 이해할 수 있도록 마이너스 처리한 것이다. 캘린더를 구현하면서 이 month값 때문에 골머리를 앓았다.

 

 

 

 

2. 콘솔 확인하기

어디서 문제가 발생했을지 모르겠을 때는 콘솔이다. 캘린더 헤더에서 currentDate와 targetDate의 콘솔을 출력해 보았다. 아래는 이전으로 날짜를 이동하면서 찍힌 콘솔 스크린샷이다. 월(Month)만 주목해서 보자. 하나씩 이전 달로 제대로 나온다. 여기서는 문제가 없다.

 

캘린더 헤더 콘솔

 

오류가 어디서 발생했는지는 금방 찾았는데, 개발하면서 신경쓰였던 부분이 있었기 때문이다. 캘린더 헤더에 오류가 발생했을 때, 꼼수같은 방법으로 해결했던 로직이 떠올랐다. 

 

선택한 날짜(target)가 현재(current)보다 미래 날짜인지, 과거 날짜인지 확인하는 로직이 있다. 현재보다 미래일 경우에는 Next 화살표(>)가 비활성화된다. 이 두 날짜를 srting으로 만들어 비교했는데, 11월이 되면 current의 자릿수가 늘어난다! 예상하지 못했던 부분이었다.

 

현재가 10월이었을 경우 가정

 

실제 오류가 발생한 콘솔

 

 

Month는 0부터 시작하기 때문에 1~12월로 달력을 쓰는 우리에게 착각을 유발한다는 점을 항상 기억하자. 달이 두 자릿수가 되면서 비교 자릿수가 달라져버렸고, 그래서 첫 두 자릿수 달인 10월이 아니라, 11월부터 오류가 발견된 것이다. 

 

// 현재 날짜보다 이전 날짜인지 확인하는 로직
  const isActiveNext = useMemo(() => {
    if (targetDate === null) {
      return false;
    }
    const targetMonth = `${targetDate.getFullYear()}${targetDate.getMonth()}`;
    const currentMonth = `${currentDate.getFullYear()}${currentDate.getMonth()}`;

    console.log('📌 currentMonth>>', currentMonth);
    console.log('➡️️ targetMonth>>', targetMonth);

    return targetMonth < currentMonth;
  }, [currentDate, targetDate]);

// ⬅️ 이전으로 이동 
  const handleChangeDateToPrev = () => {
    handleChangeTargetDate('prev');
  };

// ➡️ 다음으로 이동
  const handleChangeDateToNext = () => {
    if (isActiveNext) {
      handleChangeTargetDate('next');
    }
  };

 

 

 

 

3. 예외 상황 체크하기

무조건 뒤로만 갈 수 있는 달력인줄 알았더니, 2월이 되면 다시 앞으로 갈 수 있다. 이건 또 무슨 상황이지?

 

예외 상황

 

역시나 콘솔로 확인해보았다. 분명 current는 6자리 숫자이고, target은 5자리 숫자라 비교 자체가 안 된다. 그렇지만 나는 이 숫자를 String으로 만들어놓았고, 저 숫자처럼 보이는 글자의 타입은 String이다. 

 

위와 아래의 계산값은?

 

🔵 파란색 부분은 앞으로 이동이 불가하다. 🔴 빨간색 부분은 앞으로 갈 수 있다. 

targetMonth가 2023+2인 날짜는 2023년 3월이다. 이때는 앞으로 갈 수 없다. ❌ target이 더 크기 때문에 isActiveNext가 false다. 

targetMonth가 2023+1인 날짜는 2023년 2월이다. 이때는 앞으로 갈 수 있다. ✅ target이 더 크기 때문에 isActiveNext가 true다. 

 

String에서는 문자를 하나의 값을 지닌 덩어리로 보는 것이 아니라, 각 자리마다 고유의 값을 지닌 묶음으로 본다. 그래서 흔히 '202310' 를 숫자로 인식하는 실수를 저지를 수 있다. 파란색 부분의 경우, String의 비교 로직에 따르면 앞에서부터 2-2, 0-0, 2-2, 3-3, 1-2 이렇게 비교하게 되고, 네번째 자리에서 1과 2를 비교하니 2가 크기 때문에 target이 더 큰 문자가 된다. 문자의 크고 작음은 아스키 코드를 따른다. 

 

빨간색 부분의 경우, 앞에서부터 2-2, 0-0, 2-2, 3-3, 1-1 까지는 동일하다. 그렇지만 current에 0이 오게 됨으로써 한 자릿수 더 많은 문자가 되고, 더 큰 값으로 계산된다. 그래서 current가 더 큰 문자가 된다. current가 더 크면 앞으로 이동하는 버튼이 정상 작동한다. 

 

 

 

 

4. 오류 해결하기

지금 일어나는 문제는 Month가 한 자리였다가, 두 자리였다가 하기 때문에 발생한다. 해결 방법은 쉽다. Month를 무조건 두 자릿수로 만들어버리면 된다. 자릿수를 채우는 방법으로는 String 메소드인 padStart를 사용할 것이다. 왼쪽(문자열의 시작 부분)부터 문자열을 채우는 메소드다.

 

const getFormat2Digit = (num: number) => String(num).padStart(2, '0');

const targetMonth = `${targetDate.getFullYear()}${getFormat2Digit(targetDate.getMonth())}`;
const currentMonth = `${currentDate.getFullYear()}${getFormat2Digit(currentDate.getMonth())}`;

 

2digit로 만드는 공통 함수를 만들어 분리해 주었다. 필요에 따라 유닛 테스트를 작성할 수 있다. 오류에 비해 간단하게 해결이 되었다.

 

 

 

 

마치며

하루한냥에서 가장 핵심인 캘린더 부분에 오류가 생겨 깜짝 놀랐다. 아직 개발 단계여서 다행이라는 생각을 많이 했다. 작은 불씨도 다시 보자는 교훈도 얻었다. 프로젝트 마무리 시기였는데 갑작스런 오류에 캘린더 로직 복습도 하고 String 데이터 타입에 대해서도 학습했다. 2digit로 만드는 로직이 필요한 순간이 몇 번 있었는데 이참에 유틸 함수로 빼두었다. 그래도 비교적 수월하게 오류를 찾고 해결했기 때문에 재미있는 트러블 슈팅이었다.