IMS with TS (version2) 회고록 2

Published on Sep 6, 2024

이전에 작성한 포스팅 “IMS with TS (version2) 회고록 1”에서 이어집니다.

2-4. 코드 수정

다시 돌아와서 코드를 수정해보기 전에 목표 & 방법을 다시 정리해보자.
목표는 기존에 App.tsx에 존재하던 useReducer로 선언된 itemList, 그리고 상태 변경 로직이 App.tsx에 선언되어 어지럽던 코드를 분리하는 것이다.
코드를 분리하기 위해 다른 파일에서 전역 상태 관리 방법인 atom으로 itemListState를 선언한다.
다음으로 itemListState를 selector를 이용하여 상태를 변경한다. 이 때, 상태 변경 로직도 다른 파일에서 관리되어야 한다.
(이전 2-3에서 했던 삽질은 바로 이 부분을 처리하기 위함이다.)
자 이제 코드를 어떻게 리팩토링 했는지 살펴보자.
한가지 아이디어를 얻은 부분은 2-3 Recoil 훅 살펴보기에서 봤던 “useRecoilInterface_DEPRECATED()”의 구현 방식이다.
해당 인터페이스는 return으로 함수를 객체 형식으로 넘겨주고 있다.
나도 이 부분에서 착안하여 코드를 아래처럼 작성하였다.

// atom.ts

export const itemListState = atom<Item[]>({
  key: "itemListState",
  default: itemListMockData,
});
// selectorHooks.ts

type ReturnType =
  | {
      type: "CREATE",
      data: AddItemListProps[],
    }
  | {
      type: "UPDATE",
      data: UpdateItemListProps[],
    }
  | {
      type: "DELETE",
      data: DeleteItemListProps[],
    };

export const itemListStateSelector = selector({
  key: "itemListStateSelector",
  get: ({ get }) => get(itemListState),
  set: ({ set }, newValue) => set(itemListState, newValue),
});

export function useHandleItemList() {
  const [itemList, setItemList] = useRecoilState(itemListStateSelector);

  return ({ type, data }: ReturnType) => {
    switch (type) {
      case "CREATE":
        data.map((addItem) => {
          setItemList((prevList: Item[]) => {
            const index = findIndex(prevList, addItem.id);
            if (index === -1)
              return [
                ...prevList,
                {
                  id: addItem.id,
                  name: addItem.name,
                  barcode: addItem.barcode,
                  location: addItem.location,
                  quantity: addItem.quantity,
                  expDate: addItem.expDate,
                },
              ];
            else {
              const updateList = [...prevList];
              updateList[index].quantity += Number(addItem.quantity);
              return updateList;
            }
          });
        });
        break;
      case "UPDATE":
        data.map((updateItem) => {
          const index = findIndex(itemList, updateItem.id);
          const newId = `${itemList[index].barcode}-${updateItem.updateLocation}-${itemList[index].expDate}`;

          setItemList((prevList: Item[]) => {
            const currentIndex = findIndex(prevList, updateItem.id);
            const updateIndex = findIndex(prevList, newId);
            validateUsingBoolean(currentIndex === -1);
            const currentItemInfo = { ...prevList[currentIndex] };
            if (updateIndex === -1) {
              const updateItemInfo = {
                ...currentItemInfo,
                id: newId,
                location: updateItem.updateLocation,
                quantity: Number(updateItem.updateQuantity),
              };
              if (currentItemInfo.quantity === updateItem.updateQuantity) {
                const updateArr = prevList.filter(
                  (_, idx) => idx !== currentIndex
                );
                return [...updateArr, updateItemInfo];
              } else {
                currentItemInfo.quantity -= Number(updateItem.updateQuantity);
                const updateArr = [...prevList];
                updateArr[currentIndex] = currentItemInfo;
                return [...updateArr, updateItemInfo];
              }
            } else {
              const updateItemInfo = { ...prevList[updateIndex] };
              if (currentItemInfo.quantity === updateItem.updateQuantity) {
                const updateArr = prevList.filter(
                  (_, idx) => idx !== currentIndex
                );
                const updateIndex = findIndex(updateArr, newId);
                updateItemInfo.quantity += updateItem.updateQuantity;
                updateArr[updateIndex] = updateItemInfo;
                return [...updateArr];
              } else {
                const updateArr = [...prevList];
                currentItemInfo.quantity -= Number(updateItem.updateQuantity);
                updateItemInfo.quantity += Number(updateItem.updateQuantity);
                updateArr[currentIndex] = currentItemInfo;
                updateArr[updateIndex] = updateItemInfo;
                return [...updateArr];
              }
            }
          });
        });
        break;
      case "DELETE":
        data.map((deleteItem) => {
          const index = findIndex(itemList, deleteItem.id);
          validateUsingBoolean(index === -1);
          const updateInfo = {
            ...itemList[index],
            quantity:
              itemList[index].quantity - Number(deleteItem.DeleteQuantity),
          };
          setItemList((prevList) => {
            const updateList = [...prevList];
            if (updateInfo.quantity === 0) {
              return updateList.filter((_, idx) => idx !== index);
            } else {
              updateList[index] = updateInfo;
              return [...updateList];
            }
          });
        });
        break;
    }
  };
}


위처럼 useHandleItemList()이라는 커스텀 훅은 (type, data)을 인자로 받아 switch case문을 이용해 적절한 로직을 처리하는 함수를 반환한다.
다시 생각해보면 useHandleItemList() 훅 안에 로직을 처리하는 함수들을 모두 정의한 이후, 객체 형식으로 함수들을 리턴해주는 것도 좋은 방법이라는 생각이 든다.
useage를 간단하게 살펴보고 다음 파트로 넘어가보자.

//AddItemList.tsx

...
const handleItemFunc = useHandleItemList();

const handleSaveButton = () => {
    handleItemFunc({ type: "CREATE", data: addItemList });
    setAddItemList([]);
  };
...

2-5. TS를 왜 사용하는가?

코드를 작성 & 분석하면서 TS를 사용하는 이유를 체감할 수 있는 부분이 있었다.
우선 TypeScript를 사용하는 이유는 코드를 작성하면서 제한을 두기 위함이다.
즉, 타입을 제한하면서 코드를 작성하면 런타임이 아니라 코드를 컴파일하기 이전에 어느 부분에 문제가 존재하는지 바로 알아챌 수 있다.
아래의 사진을 한번 살펴보자.

[OrderItem Type 정의]


상황은 다음과 같다.
처음에는 OrderItem의 프로퍼티로 orderDate를 정의하고 있었다.추후 개발하면서 지시서라는 개념을 추가하기로 결정했다.
그 과정에서 orderDate가 OrderItem 내부에 있는 것은 적절하지 않다고 판단하여 OrderDirection으로 orderDate 프로퍼티를 이동시키려고 하는 상황이다.
중요한 지점은 orderItem 관련 로직이 작성된 이후 내용을 변경했다는 것이다.
다행히도 OrderItem의 type을 정의한 부분에서 orderDate를 주석처리하게되면, 직접적으로 해당 타입을 사용했던 부분을 빠르게 추적해준다.
따라서, 타입이 정의된 부분에서 코드를 수정해가면서 성공적으로 새로운 개념을 도입하여 개발을 진행할 수 있었다.
이처럼 개발을 엄격한 규칙 아래에서 작성하면 추후 유지 보수 관점에서도 이익이 있다는 것을 느끼게 되었다.

다음으로는 코드를 분석하는 과정에서 타입의 정의는 꽤 유용하게 사용될 수 있다는 것이다.
2-3에서 Recoil 훅을 분석하는 과정을 포스팅했는데 사실 생각보다 힘들었다.
처음에 필자는 방법을 2개로 나눠서 코드 분석을 시도해봤다.

1. 함수의 이름으로 기능을 대충 짐작한다 -> 이후 함수가 정의된 곳으로 계속 이동하면서 기능 파악
2. 타입 확인


결과적으로 필자가 느낀점은 위처럼 방법을 분리하는 것이 아니라 하나로 섞어서 코드 분석을 진행해야한다는 것을 여러번 하다보면서 느꼈다.
(어느 순간부터는 방법을 나눠서 보는게 아니라 근처에 여러 힌트들을 조합해서 코드를 분석하는 내 모습을 발견할 수 있었다.)
즉, 타입의 정의는 코드를 분석하는 과정에서 하나의 ‘힌트’로 작용할 수 있다.

우선 함수가 어느 타입으로 정의되었는지 파악하고 해당 타입은 어떤 구조로 이뤄졌는지 확인한다.
다음으로 파라미터가 있다면 어느 타입의 파라미터를 받는지 확인한다.
로직의 처리 흐름을 위 과정을 반복하면서 따라간다.
마지막으로 함수가 리턴하는 값의 타입을 확인한다.


내가 코드 분석을 하는 방법이 완벽하다고 생각하지도 않고, 아직 코드 분석하는 내 실력이 많이 부족하기는 하다.
하지만, 타입을 하나의 힌트로 사용하여 코드 분석을 더 쉽고 빠르게 이해할 수 있다는 것을 경험적으로 알 수 있었고, 추후에도 동일한 방법으로 계속 분석을 해보려고 한다.
(하다보면 실력이 늘거다!)

3. 마무리

이번에는 TypeScript를 사용한 IMS를 만들면서 경험을 쌓고자하는 목적으로 이번 개발을 진행해봤습니다.
IMS라는 컨텐츠로 혼자서 2번째 개발을 진행하다보니 추가적인 기능 욕심이 생겨서 Order 페이지에 기존에는 없던 여러 기능들을 추가해봤습니다.
다행히도 삽질을 하는 과정에서 상태에 대한 이해가 같이 늘어서 그런지 생각보다 어렵지 않게 코드를 작성할 수 있었습니다.
하지만, 여러 부분에서 아직 만족스럽지 못한 부분이 존재합니다.
유튜브를 보다가 선언적 프로그래밍 관련 내용을 찾아볼 수 있었고 코드를 어떤식으로 작성해야할지 계속 찾아봤습니다.
그 결과 저는 조금더 깔끔한 코드를 작성할 수 있는 한가지 아이디어를 얻었습니다.
“UI를 모두 구성하고 필요한 컴포넌트들을 정의하면서 코드를 작성하면 선언식으로 코드를 작성할 수 있을 것 같다!”
(다음으로 학교 졸업 프로젝트가 하나 있는데 여기서 한번 선언식으로 코드를 작성해보고 이 부분도 포스팅을 해보겠습니다 ㅎㅎ)
(시간 관계상 프로젝트가 끝나고나서 이번에 만든 IMS with TS (version2)를 선언식으로 리팩토링해보고자 합니다!)
제가 아이디어를 얻도록 도와준 유튜브 링크 2개와 IMS with TS (version2) 코드가 있는 github 링크를 아래에 첨부하겠습니다.

감사합니다.

리액트를 명령형으로 짜면 안되는 이유? 예시코드로 알아보자! : 가장 쉬운 웹개발 with Boaz
Full-Stack Social Media App Tutorial with React 19 & Next.js 15 & MySql : Lama Dev
github-IMS with TS (version 2)