CHZZK-Study / Grass-Diary-Client

취지직 2팀 프로젝트 Grass Diary
1 stars 3 forks source link

♻ refactor: Id 공통 타입 정리 및 컴포넌트/일기 상세페이지 타입 분리 #149

Closed rkdcodus closed 3 months ago

rkdcodus commented 3 months ago

✅ 체크리스트

📝 작업 상세 내용

1️⃣ types/diary.ts 중복 정의된 타입 제거

제가 작업했던 일기 상세페이지와 공유페이지에 관련된 타입을 정리했습니다. 정리한 타입은 크게 2가지입니다.
첫 번째로 공유페이지에서는 Top10 일기와 최신순 일기의 데이터를 불러오는 api는 다르지만 피드 내용은 동일합니다. 즉, api를 통해 받는 response 데이터 내용이 동일합니다. 그래서 Feed 타입을 새로 선언하여 중복을 제거해주었습니다.

하지만 값은 동일한 반면, 프로퍼티 이름이 달라서 api 수정 요청이 필요합니다. (diaryContent와 content) IDiary 와 IDiaryDetail 에도 id와 diaryId 프로퍼티 이름을 동일하게 하면 좋을 거 같습니다.
이에 관한 개선 사항은 노션에 간단히 적어두었습니다.

Feed 타입

interface Feed {
  diaryId: number;
  memberId: number;
  createdAt: string;
  nickname: string;
  content: string;
  diaryLikeCount: number;
}

두번째는 likedByLogInMember: boolean;을 IDiary에서 IDiaryDetail로 옮겼습니다. IDiaryDetail에서 필요하지만 IDiary에서는 필요하지 않아 제자리에 위치시켰습니다.

interface IDiaryDetail extends IDiary {
  id: Id;
  memberId: Id;
  hasImage: null;
  hasTag: null;
  imageURL: string;
  likedByLogInMember: boolean;
}

2️⃣ 통합 Id 타입 정의

diaryId number 타입으로 수정

먼저 #143 의 후속작업으로 필요했던 diaryId 타입을 변경해주었습니다. api에서 받는 diaryId는 number 타입이였고 url params로 받아오는 diaryId는 string 타입이여서 diaryId는 2가지의 타입으로 정의되어있었습니다. 이 때문에 타입에 혼란이 왔고 이 두 타입을 분리하는 것보다는 number 타입 하나로 통일해주는 것이 좋을 것 같다는 판단에 통일해주었습니다.

export const useParamsId = () => {
  const { diaryId } = useParams();
  const diaryId_num = Number(diaryId);
  if (Number.isNaN(diaryId_num)) {
    return 0;
  }

usePramsId라는 훅을 만들어서 url로 들어오는 문자열 params를 숫자로 변경해주었습니다. 만약 숫자로 변경할 수 없는 값이라면 0을 return하도록 했습니다.

memberId number 타입으로 수정

memberId의 타입은 number | null로 정의되어있었는데 number로 바꾸어주었습니다.

// grass-diary/src/recoil/user/userState.ts

// 수정 전
export const memberIdAtom = atom<number | null>({
  key: 'memberId',
  default: null,
});

// 수정 후
export const memberIdAtom = atom<number>({
  key: 'memberId',
  default: 0,
});
// grass-diary/src/recoil/user/useUser.ts

  useEffect(() => {
    if (isSuccess && memberId !== undefined) setMemberId(memberId);
    // if (!isAuthenticated) setMemberId(null); 수정 전
    if (!isAuthenticated) setMemberId(0);
  }, [isSuccess, memberId, isAuthenticated, setMemberId]);

Id 타입 정의

type Id = number;

diaryId와 memberId는 number 타입으로 동일하여 grass-diary/src/types/id.ts에 Id 타입을 정의했습니다. 모든 diaryId와 memberId의 타입 값을 number -> Id로 변경해주었습니다. Id 타입 별칭을 사용하면 Id는 항상 number 타입이여야한다는 일관성이 보장됩니다.

모든 diaryId와 memberId 타입 정의가 필요한 곳에 아래처럼 수정해두었습니다. 2번 정도 검토를 했지만 혹시라도 변경되지 않은 부분이 있다면 수정해주시면 될 것 같습니다!

diaryId: Id,
memberId: Id,

3️⃣ 일기 상세페이지 타입 분리 및 컴포넌트 props 정리

일기 상세페이지 모달 관련 함수 props 타입은 types/modal.ts 파일로,
IDiaryDetail 타입은 types/diary.ts 파일로 분리해두었습니다. 그리고 BackButton, Feed, PopularFeed, Like, NomalLike, Ellipsis, Header 컴포넌트의 props 타입을 interface로 선언하고 I[컴포넌트명]Props로 명칭을 통일시켜주었습니다.

components 폴더의 컴포넌트 props는 분리에 대해 분리를 시켜주는 것이 맞는지, 분리시킨다면 어떤 방식으로 분리할 지는 논의가 필요할 거같아 진행하지 않았습니다.

만약 분리하게 된다면 interface보다 type으로 선언하는게 나을 것 같기도 하다는 생각입니다. 왜냐하면 props는 확장 가능성이 그리 크지 않다고 생각하고 type으로 선언하면 vscode 에디터에서 마우스 오버로 타입 미리보기가 가능합니다. 파일이 분리되어있을 때 이 기능이 은근 유용할 거 같다는 생각입니다! interface로 선언했을 땐 ctrl을 누르고 봐야한다는 번거로움이 있습니다. 또한 한 파일에 props를 모아둘 경우, interface는 동일한 이름으로 타입을 선언하는 것이 가능하지만 type은 불가능합니다. 동일한 이름의 타입이 있다면 혼란을 야기할 수 있다고 생각해서 이럴 경우 type으로 선언하는 것이 좋을 것 같습니다!

그래서 types 폴더에 정의된 diaryDetail 타입들 중, 모달 관련 props 타입들은 type으로 선언해주었고 이름도 I를 제거한 [컴포넌트명]Props으로 변경했습니다!

4️⃣ diaryId 예외처리

용호님의 리뷰를 확인하고 diaryId 예외처리를 진행해주었습니다! 예외처리를 해주기 전에 유효하지 않은 diaryId 값으로 상세페이지에 접근할 시, useDiaryDetail의 AxiosError 처리로 이미 nont-existant-page로 넘어가도록 되어있었습니다. 하지만 수정페이지와 같이 diaryId를 인수로 가지는 다른 경우엔 예외처리가 되지 않고 있었습니다.

diaryId의 유효하지 않은 값은 아래와 같습니다.

이 2가지의 경우에는 non-existant-page 페이지로 이동하도록 예외처리를 해주어야합니다. 처음엔 리턴값을 0 혹은 null로 주고 예외처리를 할 생각이였지만 아예 useParamsId 훅 내부에서 예외처리를 해주면 리턴값 타입 고민 없이 깔끔한 예외처리가 가능합니다. 따라서 코드를 아래처럼 변경해주었습니다.

export const useParamsId = () => {
  const { diaryId } = useParams();
  const diaryId_num = Number(diaryId);
  const navigate = useNavigate();

  useEffect(() => {
    if (Number.isNaN(diaryId_num) || diaryId_num === 0) {
      navigate('/non-existent-page');
    }
  }, [diaryId_num]);

  return diaryId_num;
};

navigate는 페이지를 이동하는 사이드 이펙트인데 이런 사이드 이펙트는 컴포넌트 렌더링이 완료된 후에 실행되도록 useEffeact 안에 써주어야합니다. 그렇지 않으면 You should call navigate() in a React.useEffect(), not when your component is first rendered. 경고가 뜨며 useParams를 사용하는 DiaryDetail의 페이지에서도 경고가 발생합니다.

또한 일기 수정시 useParmas의 diaryId가 undefiend로 읽히는 버그를 발견하여 router의 path 수정으로 해결해주었습니다! 버그 원인은 params 변수명이 맞지 않아서 였습니다.

// 수정 전
{ path: '/editdiary/:Id', element: <CreateDiary /> },

// 수정 후 
{ path: '/editdiary/:diaryId', element: <CreateDiary /> },

이렇게 수정 후에 useParamsId가 사용되는 일기 상세페이지와 수정 페이지에서 테스트 결과, 제대로 예외처리되는 것을 확인했습니다!

🚨 버그 발생 이유 (선택 사항)

🔎 후속 작업 (선택 사항)

🤔 질문 사항 (선택 사항)

📚 참고 자료 (선택 사항)

타입스크립트에서 type 과 interface 더 알아보기

📸 스크린샷 (선택 사항)

변경 사항에 대한 스크린샷이 있다면 첨부해주세요.

✅ 셀프 체크리스트

이슈 번호: Close #148 Close #153