프로젝트/하루한냥

[하루한냥] 심리 테스트 구현 : API 호출 함수 분리하기

알파카털파카 2023. 10. 2. 19:08
[하루한냥]
심리 테스트 구현 : API 호출 함수 분리하기

 

 

하단의 4개 메뉴 중 마지막 큰 과업으로는 '심리 테스트'가 남았다. 현재는 스트레스 검사 하나뿐이지만, 추후 다른 테스트가 추가될 수 있을 여지를 남겨놓고 작업 중이다. 이번 내용은 API 함수를 필요한 페이지에서 바로 작성하는 것이 아니라, 모듈로 분리해서 사용하는 방법을 적어보려 한다. 미처 몰랐던 내용인데 멘토님께서 이런 방향으로 작업해 보라고 알려주셨다. 기존에 내가 코드를 작성했던 방법과, 리팩토링 과정을 남겨본다.

 

 


 

 

1. REPORT 플로우 

라우팅 구성은 아래 스크린샷과 같다. 우선 메인 '/report' 페이지에서는 검사를 새로 하거나, 이전의 검사결과를 볼 수 있다. 7일 이내에 검사를 중복해서 할 수 없도록 처리했기 때문에 새로 검사를 하려면 기존 7일 이내의 검사결과를 삭제해야 한다. 새로 스트레스 검사를 시작하면 10개의 문항이 있다. 모든 질문에 답변을 선택하면 결과보기 버튼이 활성화된다. 버튼을 클릭하면 검사 결과 페이지로 이동된다. 이전 결과를 모두 보고 싶다면 '이전 결과보기 보기' 버튼을 누르면 된다.

 

report 플로우

 

 

 

 

2. Axios를 활용하는 http.ts 생성하기

이 파일은 프로젝트 초기부터 만들어 사용하고 있었다. REST API 메소드를 이용해 객체를 구성헸다. 

 

// http.ts

type APIResponse<T = unknown> = {
  success: boolean;
  msg: string;
  data?: T;
};

export const http = {
  get: function get<Response = unknown>(url: string, config?: AxiosRequestConfig) {
    return client.get<APIResponse<Response>>(url, config).then((res) => res.data);
  },
  post: function post<Response = unknown, Request = any>(url: string, data?: Request, config?: AxiosRequestConfig) {
    return client.post<APIResponse<Response>>(url, data, config).then((res) => res.data);
  },
  put: function put<Response = unknown, Request = any>(url: string, data?: Request, config?: AxiosRequestConfig) {
    return client.put<APIResponse<Response>>(url, data, config).then((res) => res.data);
  },
  patch: function patch<Response = unknown, Request = any>(url: string, data?: Request, config?: AxiosRequestConfig) {
    return client.patch<APIResponse<Response>>(url, data, config).then((res) => res.data);
  },
  delete: function del<Response = unknown>(url: string, config?: AxiosRequestConfig) {
    return client.delete<APIResponse<Response>>(url, config).then((res) => res.data);
  },
};

 

 

 

 

3. 검사 결과 보기 기능 - 기존의 코드

검사 페이지에서 일어나는 로직이다. 결과보기 버튼을 눌렀을 때 사용자의 답변을 제출하고(POST), 제출한 최신 검사 결과 데이터를 불러와(GET) 해당 검사의 결과보기 페이지로 라우팅 시켜준다. 이 요청을 각각 나눠서 구현했다.

 

결과보기 버튼 활성화

 

// QuestionPage.tsx

// 모든 질문에 답변을 선택하면 답변을 등록할 수 있음 
const postCheckedAnswer = async () => {
  try {
    const response = await http.post('/answer', {
      type: 'stress',
      scores: answer,
    });

    if (response.success) {
      return response.success;
    }
  } catch (e) {
    await axiosErrorAlert(e);
    return false;
  }
};

// '검사 결과 보기' 버튼을 누르면 submit 함수가 호출됨 
const handleSubmit = async () => {
  const isValidate = await postCheckedAnswer();

  // 답변 리스트를 불러와 가장 최근의(0번째 인덱스) id를 이용해 검사 결과 페이지로 라우팅 처리 
  const getReportsList = async () => {
    try {
      const response = await http.get<{ answers: ReportAnswers[] }>('/answer');
      if (response.data) {
        const id = response.data.answers[0].answer_id;

        if (isValidate) {
          navigate(`/report/answer/${id}`);
        }
      }
    } catch (e) {
      await axiosErrorAlert(e);
    }
  };

  getReportsList();
};

 

 

 

 

4. API 호출 함수 분리하기

api 폴더 안에 각 도메인 별로 디렉토리를 만든다. 함수명에는 'api-'를 붙여 api 관련 함수임을 나타낸다. index 파일을 이용해 모듈화시켜준다. (이전 포스트 참고)

 

디렉토리 구조

 

// apiPostAnswer.ts

import { http } from '../http';

type RequestPostAnswer = {
  type: 'stress';
  scores: number[];
};

export const apiPostAnswer = (type: 'stress', scores: number[]) => {
  const data = {
    type,
    scores,
  };

  return http.post<unknown, RequestPostAnswer>('/answer', data);
};
// apiGetAnswers.ts

import { ReportAnswers } from '@lib/types';
import { http } from '../http';

type ResponseGetAnswers = {
  answers: ReportAnswers[];
};

export const apiGetAnswers = () => http.get<ResponseGetAnswers>('/answer');

 

이 함수를 가져다 사용하는 곳에서 async/await를 붙이면 된다. POST의 경우 필요한 인자를 넣어 사용할 수 있다. 반환값으로는 해당 결과가 담긴 프로미스 객체가 리턴되므로, 사용할 때 데이터를 받아서 풀어 쓰면 된다. 

 

 

 

 

5. 검사 결과 보기 기능 - 리팩토링 후 코드

분리한 API 함수를 적용했다. 결과보기 버튼을 클릭할 때 submit 함수가 호출되므로 이 함수 안에 답변 제출(POST)과 결과보기(GET)를 모두 넣어놓았다. 

 

const handleSubmit = async () => {
  try {
    const responsePostAnswer = await apiPostAnswer('stress', answer);

    if (responsePostAnswer.success) {
      const responseGetAnswers = await apiGetAnswers();

      if (responseGetAnswers.success && responseGetAnswers.data) {
        const id = responseGetAnswers.data.answers[0].answer_id;
        navigate(`/report/answer/${id}`);
      }
    }
  } catch (e) {
    await axiosErrorAlert(e);
  }
};

 

새로운 검사 기록은 항상 맨 앞에 추가되기 때문에 [0]번째 인덱스를 이용해 id 정보를 불러오고 있다. try/catch도 한 번만 사용하면 된다. 코드의 양도 줄어 깔끔해졌다. 같은 함수가 다른 페이지에서 필요할 때도 import 해서 사용하면 되어 편리해졌다. 

 

responsePostAnswer와 responseGetAnswers 콘솔

 

 

 

 

마치며

함수 이름에 'api-'를 붙여서 비동기 통신에 사용되는 함수임을 명확히 드러내는 점이 편리하게 느껴졌다. 매 페이지마다 중복으로 비동기 처리할 때 동일한 API를 연동하는 것이 은은하게 신경쓰였는데, 이렇게 함수로 분리해 놓으니 중복되는 코드를 줄일 수 있고 재활용 가능해서 유용했다. 한편으로는 API 연동하면서 타입 지정하는 것을 자주 놓치는데, 아예 이 함수를 분리해놓으니 타입 이슈가 줄었다. 확실히 경력자의 멘토링을 받는 것이 얼마나 감사한 일인가 생각하게 된다. 😊