[Typescript pair study] my AgoraStates Typescript 리팩토링 코드 공유 세션 ②

이민선(Jasmine)·2023년 5월 20일
1
post-thumbnail

4번째 세션!

이번주는 저번주에 각자 설정한 목표대로 my Agora States 과제의 React/Typescript 리팩토링을 계속하고 진행 상황을 공유했다.

💡 나의 과제

이번 주 진행 상황 요약

Paginations.tsx

  • 질문 수정, 삭제 기능 추가

store.tsx

  • local storage에 데이터 연동
  • 질문 추가, 수정, 삭제 시 toast 기능 추가

Type 발표

1. 수정 또는 삭제할 원소를 find 메서드로 찾아 할당할 변수에 tuple type 지정

// 수정 버튼에 handleUpdate 함수 연결
  const handleUpdate = (id: number, currentTitle: string) => {
.
.
(중략)
.
.
    const discussionToUpdate: Discussion | undefined = state.find(
      (discussion: Discussion) => discussion.id === id
    );
.
.
(중략)
.
.

  };
// 삭제 버튼에 handleDelete 함수 연결
  const handleDelete = (id: number) => {
    if (isUpdateBtnClicked) {
      setIsUpdateBtnClicked(false);
      return;
    }
    const targetItem: Discussion | undefined = state.find(
      (item: Discussion) => item.id === id
    );
    if (targetItem) dispatch(deleteDiscussion(targetItem));
  };

여기에서 state은 redux store에서 가져온 discussions 배열을 의미한다. state에서 함수의 인자로 받은 id와 일치하는 discussion을 찾는데, 이 때 typescript compiler는 찾고자 하는 item이 없을 가능성이 있다고 판단한다.

위의 사진처럼 targetItem 변수에 Discussion이라는 type만 확정적으로 적용하면 type 에러가 나기 때문에 tuple을 이용하여 이론적으로는 undefined일 수도 있다고 type 시스템에 명시해주었다.

2. local storage에 있는 데이터를 reducer의 initial state로 받아올 때 Alias 배열 type인 Discussion[] 지정

const initialDiscussionStateLocalStorage: Discussion[] = [];

저번주에 서버에서 가져온 데이터를 local storage에 연동하는 로직을 추가해서 reducer의 초기값을 변경했는데, local storage를 순회하며 discussion을 추가하기 위해 선언한 빈 배열에 Discussion[] type을 지정해주었다.

3. localStorage에 있는 데이터를 불러올 때 타입 가드 적용

for (let key in localStorage) {
  const value = localStorage.getItem(key);
  // value가 undefined일 경우 조건문 진입하지 않음.
  if (value) {
    const parsedValue = JSON.parse(value);
 // parsedValue에 author 속성이 undefined일 경우 조건문 진입하지 않음.
    if (parsedValue.author) {
      initialDiscussionStateLocalStorage.push(parsedValue);
    }
  }
}

localStorage 내부에 value가 undefined일 가능성이 있고, JSON.parse를 적용하여 가져온 객체들 중에 author 속성이 없는 객체가 이론적으로는 존재할 수도 있으므로 undefined일 경우 조건문에 진입하지 않는 타입 가드를 적용했다.

4. toast를 띄울 때 함수에 인자로 전달하는 action type명과 toast 메시지를 각각 union type으로 관리

// toastify 함수에 인자로 전달할 수 있는 action type의 목록
export type ToastAction = "add" | "update" | "delete";

// action type에 따라 toast에 띄워줄 메시지의 목록
export type ToastMsg =
  | "질문이 추가되었습니다."
  | "질문이 수정되었습니다."
  | "질문이 삭제되었습니다.";

// toast 메시지를 각각 변수에 담고 ToastMsg 타입 적용
const addMsg: ToastMsg = "질문이 추가되었습니다.";
const updateMsg: ToastMsg = "질문이 수정되었습니다.";
const deleteMsg: ToastMsg = "질문이 삭제되었습니다.";

// toastify 함수에서 action type을 매개변수로 받을 때 ToastAction type 적용
const tostify = (actionType: ToastAction) =>
  toast(
    actionType === "add"
      ? addMsg
      : actionType === "update"
      ? updateMsg
      : deleteMsg,
    { position: toast.POSITION.TOP_RIGHT }
  );

export const discussion = createSlice({
  name: "discussionReducer",
  initialState: initialDiscussionStateLocalStorage,
  reducers: {
    createDiscussion: (
      state: Discussion[],
      action: PayloadAction<Discussion>
    ) => {
      localStorage.setItem(
        String(action.payload.id),
        JSON.stringify(action.payload)
      );
 // createDiscussion action 메서드 내부에서 toastify 함수에 add toast action 전달
      tostify("add");

      return [action.payload, ...state];
    },
    deleteDiscussion: (
      state: Discussion[],
      action: PayloadAction<Discussion>
    ) => {
      localStorage.removeItem(String(action.payload.id));
      
 // deleteDiscussion action 메서드 내부에서 toastify 함수에 delete toast action 전달
      tostify("delete");

      return state.filter((item: Discussion) => item.id !== action.payload.id);
    },
    updateDiscussion: (
      state: Discussion[],
      action: PayloadAction<Discussion>
    ) => {
      localStorage.removeItem(String(action.payload.id));
      localStorage.setItem(
        String(action.payload.id),
        JSON.stringify(action.payload)
      );
 // updateDiscussion action 메서드 내부에서 toastify 함수에 update toast action 전달
      tostify("update");

      return state.map((discussion: Discussion) =>
        discussion.id === action.payload.id
          ? { ...discussion, title: action.payload.title }
          : discussion
      );
    },
  },
});

reducer 내부에 정의해 놓은 action 메서드가 실행될 때마다 toastify 함수를 호출하는데, 이 때 전달할 수 있는 action명이 string type이기 때문에 union type으로 관리한다. 그리고 toastify 함수에서 띄워줄 메시지는 action type에 따라 각각 addMsg, updateMsg, deleteMsg 변수에 저장해놓았는데, 각각의 메시지도 동일한 이유로 alias로 type으로 관리한다.

이번 세션 회고

coz-shopping solo project를 하면서 react 실력이 많이 늘었다고 생각했는데, type을 적용하는 실력도 못지 않게 많이 향상되었다고 느꼈다. 이제야 비로소 typescript compiler만 통과하는 데에 급급한 type 적용이 아니라, typescript를 활용하고 있다는 느낌이 든다. type error를 고마워하게 될 줄이야..! any 덕후이던 시절에는 몰랐지만, type error가 나의 그릇된 코드를 올바른 길로 인도해주는 경우를 여러번 만나면서 안정적으로 개발하고 있다는 생각이 들기 시작했다. 예를 들자면 useState 사용 시 type을 미리 지정해놓았기 때문에 type 실수 없이 setState을 할 수 있다.

다음주에는 드디어 제네릭! 제네릭 기초 세션에서 나는 함수에 제네릭을 적용해본 코드를, 수민님은 클래스에 제네릭을 적용해본 소스 코드를 만들어와서 발표할 예정이다. 아직은 제네릭이 뭔지도 정확히 모르면서 그 때 그 때 맞춰서 사용하는 것 같은데, 좀 더 정확히 개념을 잡아봐야겠다.

수민님은 차근차근 성장 중이시다!! 오늘 리액트를 다루면서 어려웠던 점들을 공유해주셨고, 에러 메시지를 참고해서 같이 뚱땅뚱땅 고쳐보았다. my Agora States을 만든 김에 수민님과 질문이나 어려웠던 점을 함께 공유하는 공간으로 활용해보면 어떨까...?? (수민님 어떠신가요?!! ㅋㅋㅋㅋㅋ)

📢 수민님 이번주 solo 프로젝트랑 저희 스터디 과제 둘 다 하는 거 빡셌을텐데ㅠㅠ 잘 따라와주셔서 감사했고 또 수고 많으셨어요! 다음주도 화이팅 😊👍😊👍

profile
기록에 진심인 개발자 🌿

0개의 댓글