ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [넘블] DataFetching 모듈 설계하기
    자바스크립트 2022. 6. 30. 20:35

    아래의 AuthService 클래스는 인증 관련 비즈니스 로직들을 다루는 모듈입니다. 기능적으로 문제없지만 리팩토링할 수 있는 부분들은 있습니다. 

    class AuthService {
      /** refreshToken을 이용해 새로운 토큰을 발급받습니다. */
      async refresh() {
        const refreshToken = cookies.get("refreshToken");
        if (!refreshToken) {
          return;
        }
    
        const { data } = await axios.post(
          process.env.NEXT_PUBLIC_API_HOST + "/auth/refresh",
          null,
          {
            headers: {
              Authorization: `Bearer ${refreshToken}`,
            },
          }
        );
    
        cookies.set("accessToken", data.access, { expires: 1 });
        cookies.set("refreshToken", data.refresh, { expires: 7 });
      }
    
      /** 새로운 계정을 생성하고 토큰을 발급받습니다. */
      async signup(
        email: string,
        password: string,
        name: string,
        phoneNumber: string,
        agreements: SignupAgreements
      ) {
        const { data } = await axios.post(
          process.env.NEXT_PUBLIC_API_HOST + "/auth/signup",
          { email, password, name, phoneNumber, agreements }
        );
    
        cookies.set("accessToken", data.access, { expires: 1 });
        cookies.set("refreshToken", data.refresh, { expires: 7 });
      }
    
      /** 이미 생성된 계정의 토큰을 발급받습니다. */
      async login(email: string, password: string) {
        const { data } = await axios.post(
          process.env.NEXT_PUBLIC_API_HOST + "/auth/login",
          { email, password }
        );
    
        cookies.set("accessToken", data.access, { expires: 1 });
        cookies.set("refreshToken", data.refresh, { expires: 7 });
      }
    }

    우선 axios 라이브러리와의 강한 결합이 가장 눈에 보입니다. 현재 이는 문제 없지만 추후에 다른 data fetching 라이브러리를 사용하게 된 경우 각 기능마다 하나하나 수정을 해야하기 때문에, 재사용성에서 문제가 될 수 있습니다. 따라서 fetch api 및 다른 data fetching 라이브러리로 쉽게 바꿀 수 있다는 가정하에 코드를 수정했습니다.

     

    RestAPI는 기본적으로 post, get, put, patch, delete 기능을 지니기에 이를 바탕으로 공통으로 사용할 수 있는 클래스를 만들었습니다.

    abstract class HTTPClient<T> {
      instance: T;
    
      constructor(instance: T) {
        this.instance = instance;
      }
    
      abstract usePost<T = any, D = any>(url: string, dataObject?: any, config?: any): Promise<any>;
      abstract useGet<T = any, D = any>(url: string, config?: any): Promise<any>;
      abstract usePut<T = any, D = any>(url: string, dataObject?: any, config?: any): Promise<any>;
      abstract usePatch<T = any, D = any>(url: string, dataObject?: any, config?: any): Promise<any>;
      abstract useDelete<T = any, D = any>(url: string, config?: any): Promise<any>;
    }

     

     

    이를 바탕으로 axios 라이브러리 클래스를 작성했습니다.

    import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from "axios";
    import { HTTPClient } from "../../types/service";
    
    const axiosInstance = axios.create({
      baseURL: `${process.env.NEXT_PUBLIC_API_HOST}`,
    });
    
    class HttpClientAxios extends HTTPClient<AxiosInstance> {
      constructor() {
        super(axiosInstance);
        this.initializeResponseInterceptor();
      }
    
      async usePost<T, D>(url: string, dataObject?: D, config?: AxiosRequestConfig<D>) {
        const { data } = await this.instance.post<T, AxiosResponse<T>, D>(url, dataObject, config);
        return data;
      }
    
      async useGet(url: string, config?: AxiosRequestConfig<any>) {
        const { data } = await this.instance.get(url, config);
        return data;
      }
    
      async usePatch<T, D>(url: string, dataObject?: D, config?: AxiosRequestConfig<D>) {
        const { data } = await this.instance.patch<T, AxiosResponse<T>, D>(url, dataObject, config);
        return data;
      }
    
      async useDelete(url: string, config?: AxiosRequestConfig<any>) {
        const { data } = await this.instance.delete(url, config);
        return data;
      }
    
      async usePut<T, D>(url: string, dataObject?: D, config?: AxiosRequestConfig<D>) {
        const { data } = await this.instance.put<T, AxiosResponse<T>, D>(url, dataObject, config);
        return data;
      }
    
      initializeResponseInterceptor = () => {
        this.instance.interceptors.response.use(
          response => response,
          error => {
            // 에러 종류에 따라 일괄적으로 처리 가능
            if (axios.isAxiosError(error)) {
              // axios error 종류 및 status에 따른 일괄적인 처리 가능
            } else {
              // not axios error
            }
            return Promise.reject(error);
          }
        );
      };
    }
    
    export default HttpClientAxios;

     

    axios.create()를 활용하여 기본적인 baseURL을 설정하여 instance를 생성했습니다. 추후에 필요시 여기에 기본적인 세팅을 수정할 수 있습니다. 또한 axios의 기능인 interceptors를 사용하여 에러 핸들링에 사용할 수 있도록 했습니다. 만약에 data fetching 관련 에러일 경우 status number에 따라 일괄적으로 처리할 수가 있습니다. 그럼에도 불구하고 Promise.reject(error)를 리턴하는 이유는 react-query를 사용할 예정이기 때문입니다. react-query를 활용하면 data fetching에서 error을 리턴 받는 경우 error 관련 컴포넌트를 리턴할 수 있습니다.

     

    axios 라이브러리의 use, post 등이 리턴하는 response는  아래와 같은 properties를 리턴합니다. 그러나 다른 data fetching 라이브러리들은 다른 properties를 리턴할 수 있기에 우선 가장 필요한 data만 리턴할 수 있도록 했습니다. 

    {
      data: {},
      status: 200,
      statusText: 'OK',
      headers: {},
      config: {},
      request: {}
    }

     

    만약에 fetch api 클래스에서 useGet 함수를 오버라이딩해야한다면 간단하게 아래와 같이 작성하면 될것 같습니다. 현재 config관련 정보를 넣을 수 있지 못하지만, config 정보를 넣을 수 있는 공통 함수를 만들어 넣을 수 있도록 만들면 되지 않을까 싶습니다.

    async useGet(url: string, config?: any) {
        const res = await fetch(url);
        const data = await res.json()
        return data;
      }

     

    현재까지 만든 HttpClientAxios를 AuthService가 extend하도록 한다면 아래와 같을 것입니다.

    import cookies from "js-cookie";
    import HttpClientAxios from "./HttpClientAxios";
    import { CookieData, Signup, Login } from "../../types/service";
    
    class AuthService extends HttpClientAxios {
      constructor() {
        super();
      }
    
      /** refreshToken을 이용해 새로운 토큰을 발급받습니다. */
      async refresh() {
        const refreshToken = cookies.get("refreshToken");
        if (!refreshToken) {
          return;
        }
        const data = await this.usePost<CookieData, null>("/auth/refresh", null, {
          headers: {
            Authorization: `Bearer ${refreshToken}`,
          },
        });
        console.log("refresh", data);
    
        cookies.set("accessToken", data.access, { expires: 1 });
        cookies.set("refreshToken", data.refresh, { expires: 7 });
      }
    
      /** 새로운 계정을 생성하고 토큰을 발급받습니다. */
      async signup({ email, password, name, phoneNumber, agreements }: Signup) {
        const data = await this.usePost<CookieData, Signup>("/auth/signup", {
          email,
          password,
          name,
          phoneNumber,
          agreements,
        });
        console.log("signup", data);
    
        cookies.set("accessToken", data.access, { expires: 1 });
        cookies.set("refreshToken", data.refresh, { expires: 7 });
      }
    
      /** 이미 생성된 계정의 토큰을 발급받습니다. */
      async login({ email, password }: Login) {
        const data = await this.usePost<CookieData, Login>("/auth/login", { email, password });
        console.log("login", data);
    
        cookies.set("accessToken", data.access, { expires: 1 });
        cookies.set("refreshToken", data.refresh, { expires: 7 });
      }
    }
    
    export default new AuthService();

     

    하지만 현재까지 만든 AuthService를 그대로 사용한다면 너무나 하위 모듈에 종속됩니다. 모듈간의 의존성이 높아지면 재사용성과 확장성에 문제가 있을 수 있기에 의존성 역전 원칙을 활용하여 수정했습니다. UseRequest 클래스를 만들어 의존성을 외부에서 주입할 수 있도록 했습니다. UseRequest 클래스의 constructor에 어느 data fetching 모듈을 넣느냐에 따라 쉽게 변경 가능합니다.

    import HttpClientAxios from "../services/HttpClientAxios";
    import authService from "../services/auth.service";
    import userService from "../services/user.service";
    import { HTTPClient } from "../../types/service";
    
    class UseRequest {
      private httpReqType: any = null;
    
      constructor(httpReqType: HTTPClient<any>) {
        this.httpReqType = httpReqType;
      }
    
      authService = new authService(this.httpReqType);
      userService = new userService(this.httpReqType);
    }
    
    export const useRequest = new UseRequest(new HttpClientAxios());

     

    AuthService 클래스 또한 이에 맞게 수정했습니다.

    class AuthService {
      private httpReqType: HTTPClient<any>;
    
      constructor(HttpReqType: HTTPClient<any>) {
        this.httpReqType = HttpReqType;
      }
    
      /** refreshToken을 이용해 새로운 토큰을 발급받습니다. */
      async refresh() {
        const refreshToken = cookies.get("refreshToken");
        if (!refreshToken) {
          return;
        }
        const data = await this.httpReqType.usePost<CookieData, null>("/auth/refresh", null, {
          headers: {
            Authorization: `Bearer ${refreshToken}`,
          },
        });
        console.log("refresh", data);
    
        cookies.set("accessToken", data.access, { expires: 1 });
        cookies.set("refreshToken", data.refresh, { expires: 7 });
      }
    
      /** 새로운 계정을 생성하고 토큰을 발급받습니다. */
      async signup({ email, password, name, phoneNumber, agreements }: Signup) {
        const data = await this.httpReqType.usePost<CookieData, Signup>("/auth/signup", {
          email,
          password,
          name,
          phoneNumber,
          agreements,
        });
        console.log("signup", data);
    
        cookies.set("accessToken", data.access, { expires: 1 });
        cookies.set("refreshToken", data.refresh, { expires: 7 });
      }
    
      /** 이미 생성된 계정의 토큰을 발급받습니다. */
      async login({ email, password }: Login) {
        const data = await this.httpReqType.usePost<CookieData, Login>("/auth/login", { email, password });
        console.log("login", data);
    
        cookies.set("accessToken", data.access, { expires: 1 });
        cookies.set("refreshToken", data.refresh, { expires: 7 });
      }
    }

     

    실제 사용시 아래와 같이 사용할 수 있습니다.

    const { data: me } = useQuery("me", useRequest.userService.me, {
        refetchInterval: 500,
      });

    깃허브

     

    참고한 글들

    • https://levelup.gitconnected.com/enhance-your-http-request-with-axios-and-typescript-f52a6c6c2c8e
    • https://dev.to/mmcshinsky/a-simple-approach-to-managing-api-calls-1lo6
    • https://github.com/axios/axios/issues/1510
    • https://minhyeong-jang.github.io/2020/01/08/js-axios-interceptors-error
    • https://blog.bitsrc.io/setting-up-axios-interceptors-for-all-http-calls-in-an-application-71bc2c636e4e
    • https://velog.io/@roo333/SOLID-%EC%9D%98%EC%A1%B4-%EC%97%AD%EC%A0%84-%EC%9B%90%EC%B9%99DIP
    • https://seokzin.tistory.com/entry/JavaScript-%ED%81%B4%EB%A6%B0-%EC%BD%94%EB%93%9C-6-SOLID

    '자바스크립트' 카테고리의 다른 글

    클로저  (0) 2021.09.04
    프로토타입 체인 & toString()  (0) 2021.08.31
Designed by Tistory.