Skip to content

라이브러리 검토

jinyoung edited this page Oct 8, 2024 · 1 revision

Immer

현재 여행 계획에 대한 데이터 구조가 매우 복잡한 상태 입니다. (3중으로 중첩된 객체 구조)

// example
{
  "days": [
    {
      "places": [
        {
          "placeName": "잠실한강공원",
          "position": {
            "lat": "37.5175896",
            "lng": "127.0867236"
          },
          "todos": [
            {
              "content": "함덕 해수욕장 산책",
              "isChecked": true
            }
          ]
        }
      ]
    }
  ]
}

todos 내 todo의 content를 변경하는 과정에서 너무 많은 선작업들이 있어 번거롭다고 느끼게 되었습니다.

const onChangeContent = ({
    content,
    dayIndex,
    placeIndex,
    todoId,
  }: {
    content: string;
    dayIndex: number;
    placeIndex: number;
    todoId: string;
  }) => {
    setTravelPlanDays((prevTravelPlansDays) => {
      // 1. 이전 여행 계획 일정의 얕은 복사본 생성
      const newTravelPlans = [...prevTravelPlansDays];

      // 2. 지정된 일자와 장소에 해당하는 place 객체 찾기
      const place = newTravelPlans[dayIndex]?.places[placeIndex];

      // 3. place 객체나 todos 배열이 없으면 이전 상태 반환
      if (!place?.todos) return prevTravelPlansDays;

      // 4. todos 배열에서 지정된 todoId와 일치하는 todo의 인덱스 찾기
      const todoIndex = place.todos.findIndex((todo) => todo.id === todoId);

      // 5. 일치하는 todo가 없으면 이전 상태 반환
      if (todoIndex === -1) return prevTravelPlansDays;

      // 6. todos 배열을 새로 매핑하여 지정된 todo의 content 업데이트
      place.todos = place.todos.map((todo, index) =>
        index === todoIndex
          ? {
              ...todo,
              content: content.slice(
                FORM_VALIDATIONS_MAP.title.minLength,
                FORM_VALIDATIONS_MAP.title.maxLength,
              ),
            }
          : todo,
      );

      // 7. 업데이트된 여행 계획 일정 반환
      return newTravelPlans;
    });
  };

따라서 이러한 번거로움을 위해, 데이터 구조 변경의 필요성을 느끼게 되었습니다.

https://react.dev/learn/updating-objects-in-state#write-concise-update-logic-with-immer에 따르면 복잡한 객체 구조에 대해 use-immer를 사용하도록 권장하고 있습니다.

use-immer로 변경한 onChangeContent는 아래와 같습니다!

const onChangeContent = ({
  content,
  dayIndex,
  placeIndex,
  todoId,
}: {
  content: string;
  dayIndex: number;
  placeIndex: number;
  todoId: string;
}) => {
  setTravelPlanDays((previousTravelPlanDays) => {
	  // todo로 바로 접근
    const todo = previousTravelPlanDays[dayIndex]?.places[placeIndex]?.todos?.find(
      (todo) => todo.id === todoId,
    );

		// todo가 존재하면 todo.content에 content 추가
    if (todo) {
      todo.content = content.slice(
        FORM_VALIDATIONS_MAP.title.minLength,
        FORM_VALIDATIONS_MAP.title.maxLength,
      );
    }
  });
};

장점은 아래와 같다고 느꼈습니다.

  1. 간결한 상태 업데이트 로직
    • 복잡한 중첩 객체나 배열을 직접 수정하는 것처럼 코드를 작성할 수 있다.
    • 스프레드 연산자나 Object.assign() 등으로 복사하는 코드를 줄일 수 있다.
  2. 가독성
    • 상태 업데이트 로직이 더 직관적이고 명확해진다.
  3. 불변성 자동 유지
    • Immer가 내부적으로 불변성을 보장하므로, 휴먼 에러를 줄일 수 있다.

Axios

axios가 필요한 이유

현재 팀의 상황

  1. 백엔드 5, 프론트엔드 3인 상황에서 프론트엔드가 기능 구현이 뒤쳐지는 상황입니다.
    • 프론트엔드가 기능 개발을 위해 시간 투자를 많이 해야합니다.
  2. 현재 팀에서 스프린트 구현이 완료 된 상태에서, 인증 & 인가까지 추가해야 합니다.
    • 인가 처리가 필요한 부분 - 여행기 등록, 여행 계획 조회, 여행 계획 등록
    • 추후 각 기능의 수정 & 삭제에 대해서도 인가 처리가 필요

그래도 라이브러리를 최소화 하기 위해 APIClient 객체를 어떻게든 재활용해보려고 했지만 만약 재활용하게 된다면 기능 개발에 제약이 있을거 같다고 생각했습니다.

이유는 아래와 같습니다.

휴먼 에러 방지

  • APIClient

    import HTTPError from "@errors/HTTPError";
    
    export default class APIClient {
      static validateResponse(response: Response, errorMessage: string) {
        if (!response.ok) {
          throw new HTTPError(response.status, errorMessage);
        }
      }
    
      static async get<T>(endpoint: string, headers: Record<string, string> = {}, body?: T) {
        return this.request<T>("GET", endpoint, body ?? null, headers);
      }
    
      static async post<T extends Record<string, unknown>>(
        endpoint: string,
        body?: T,
        headers: Record<string, string> = {},
      ) {
        return this.request<T>("POST", endpoint, body ?? null, headers);
      }
    
      static async patch<T extends Record<string, unknown>>(
        endpoint: string,
        body?: T,
        headers: Record<string, string> = {},
      ) {
        return this.request<T>("PATCH", endpoint, body ?? null, headers);
      }
    
      static delete(endpoint: string, headers: Record<string, string> = {}) {
        return this.request("DELETE", endpoint, null, headers);
      }
    
      static async request<T>(
        method: "GET" | "DELETE" | "PATCH" | "POST",
        endpoint: string,
        body: T | null,
        headers: Record<string, string> = {},
      ): Promise<Response> {
        const url = `${process.env.REACT_APP_BASE_URL}${endpoint}`;
    
        const options = {
          method,
          headers: {
            "Content-Type": "application/json",
    
            ...headers,
          },
          body: body ? JSON.stringify(body) : null,
        };
    
        const response = await fetch(url, options);
    
        return response;
      }
    }

여기서 만약 인증 처리가 필요하다면 다음과 같은 선택지가 있다고 생각했습니다.

  1. 별도의 AuthAPIClient 추가
  2. APIClient에 type 추가

1번의 경우 코드 중복이 너무 심각할 것이라고 생각했고, 2번의 선택지로 코드를 구성해보았습니다.

  • code

    // ...
    
    export default class APIClient {
    	// ...
      static async get<T>(
     		// ...
        isAuth?: boolean,
      ) {
        return this.request<T>("GET", endpoint, body ?? null, headers, isAuth);
      }
    
      static async post<T extends Record<string, unknown>>(
    		// ...
        isAuth?: boolean,
      ) {
        return this.request<T>("POST", endpoint, body ?? null, headers, isAuth);
      }
    
      static async patch<T extends Record<string, unknown>>(
    		// ...
        isAuth?: boolean,
      ) {
        return this.request<T>("PATCH", endpoint, body ?? null, headers, isAuth);
      }
    
      static delete(endpoint: string, headers: Record<string, string> = {}, isAuth?: boolean) {
        return this.request("DELETE", endpoint, null, headers, isAuth);
      }
    
      static async request<T>(
    		// ...
        isAuth?: boolean,
      ): Promise<Response> {
    		// ...
        const getToken = () => localStorage.getItem("tourootAccessToken");
    
    		// ...
        const options = {
          method,
          headers: {
            ...headers,
            ...(isAuth ? { Authorization: `Bearer ${getToken()}` } : {}),
          },
          body: body ? JSON.stringify(body) : null,
        };
      }
    }
    // non-auth
    const response = await APIClient.get('/some-endpoint');
    
    // auth
    const response = await APIClient.get('/some-endpoint', {}, {}, true);

    2번의 경우에도 isAuth를 별도로 개발자가 추가해줘야하는 단점이 있으며 휴먼 에러의 가능성이 발생할 수 있다고 생각했습니다. (만약 params를 객체 형태로 변경해도 마찬가지라고 생각합니다.)

accessToken 만료 로직 추가

현재 로직 뿐만 아니라 추후 accessToken 만료 시 refresh token 로직을 apiClient에 추가해야하는 상황인데, 이 로직 추가 자체가 러닝 커브가 많이 높을 것이라고 팀원 들과 판단하게 되었습니다.

Axios 라이브러리 설치 시 계획

  1. authClient(인증 & 인가 처리)와 client 객체를 구분 지을 예정입니다.
  2. authClient의 경우 interceptor를 추가할 예정입니다.
    • accessToken을 Authorization 헤더에 추가
    • accessToken 만료시 refresh token을 통해 accessToken 재 발급
    • api error 발생 시 모니터링 도구를 통해 logging 하는 것 추가 및 httpError 추가

2번의 경우 모두 interceptor 내부에서 처리 가능하므로 개발자는 authClient만 사용하면 내부에서 자동으로 처리하여 기능 개발에 집중이 가능할 것 같다고 생각했습니다.

팀원들이 별도로 신경 쓸 필요 없이 내부에서 자동으로 처리하게 함으로써, 기능 개발에 집중할 수 있는 것을 기대하고 있습니다 🙂