-
[넘블] 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