프론트엔드 개발을 할 때 우리는 많은 라이브러리를 사용하게 됩니다.
저 같은 경우는 요즘 Axios와 React-Query는 거의 항상 사용하는 것 같아요. React는 기본..!
그러다 최근 이런 생각이 들었습니다.
"이런 라이브러리들의 새 버전을 프로젝트에 적용하게 되면 어떻게 될까?"
특수한 경우에는 해당 라이브러리에 의존하는 많은 부분의 코드를 변경해야 할 수도 있지 않을까요?
물론, 외부 요소에 하나도 의존을 하지 않고 애플리케이션을 만들 수는 없습니다.
예를 들어 브라우저에서 제공하는 기능을 이용해야 한다는 사실은 무시할 수 없으니까요.
하지만 외부 요소에 직접적으로 의존하는 코드를 최소화하고, 전체적인 제어권을 우리의 애플리케이션 안으로 가져올 수는 있습니다.
Axios, React-Query 라이브러리와 localStorage Web API를 예시로 살펴봅시다. (제가 실제 프로젝트에서 사용한 예시들입니다)
Axios 의존성 낮추기
먼저 Axios 이용에서 최종적으로 어떤 기능이 필요한지 추상적으로 정의해봅시다.
- HTTP Get 요청
- HTTP Post 요청
- 요청 시 헤더 내 인증 토큰 삽입
- 응답 시 "response success!" 출력
그리고 Axios를 이용하여 정의한 기능들을 구현한 인터페이스를 생성해봅시다.
import Axios, { AxiosRequestConfig } from 'axios';
export default class HttpClient {
private axios;
private readonly BASE_URL = 'your server base url';
private readonly AUTH_TOKEN = 'your auth token';
constructor() {
this.axios = Axios.create({ baseURL: this.BASE_URL });
this.setRequestInterception();
this.setResponseInterception();
}
protected async get<Response = unknown>(url: string, config?: AxiosRequestConfig) {
const res = await this.axios.get<Response>(url, config);
return res.data;
}
protected async post<Response = unknown, Request = any>(url: string, body?: Request, config?: AxiosRequestConfig) {
const res = await this.axios.post<Response>(url, body, config);
return res.data;
}
private setRequestInterception() {
this.axios.interceptors.request.use(config => {
config.headers.Authorization = this.AUTH_TOKEN;
return config;
});
}
private setResponseInterception() {
this.axios.interceptors.response.use(value => {
console.info('response success!');
return value;
});
}
}
이제 생성한 위 HttpClient 클래스를 이용하여 모든 HTTP 요청 및 응답을 관리하고, 각 모듈에서는 Axios를 직접적으로 호출하지 않음으로써 Axios 라이브러리에 대한 의존성을 줄일 수 있습니다.
만약 Axios 관련 변경이 발생하더라도 HttpClient 클래스 수정만 수행하면 되겠죠.
HttpClient 클래스 내 get, post 메서드의 인자의 일부 타입으로 Axios에서 제공하는 AxiosRequestConfig 타입을 사용하고 있기 때문에, 여전히 다른 모듈에서 Axios에 의존하는 경향이 조금은 있어보이긴 하네요..
React-Query 의존성 낮추기
이번엔 제가 실제 프로젝트에서 React-Query 라이브러리 의존성을 낮춘 방법을 소개하겠습니다.
useQuery, useMutation, useInfiniteQuery Hook 및 fetchQuery 메서드를 커스텀하여 정의했지만, 이 중 useQuery Hook 하나만 살펴보겠습니다.
먼저 커스텀할 useQuery Hook의 인자 타입을 정의합니다.
import { QueryKey, UseQueryOptions } from '@tanstack/react-query';
import ExceptionBase from '@/lib/HttpErrorException';
export interface Query<TQueryFnData> {
queryKey: QueryKey;
queryFn: () => TQueryFnData | Promise<TQueryFnData>;
options?: UseQueryOptions<TQueryFnData, ExceptionBase>;
}
위에서 살펴본 Axios의 예시와 같이, React-Query 라이브러리에서 제공하는 QueryKey, UseQueryOptions 타입을 사용하고 있기에 다른 모듈들에서 React-Query에 대한 의존성이 발생할 여지가 있습니다.
이와 같이 외부 라이브러리의 타입을 그대로 사용하는 것에 대한 조치는 더 고민해봐야겠어요..🤔
이제 위에서 생성한 타입을 이용하여 useQuery Hook을 재정의합니다.
import { useQuery as useQueryOrigin } from '@tanstack/react-query';
import ExceptionBase from '@/lib/HttpErrorException';
import { Query } from '../types/query';
const useQuery = <TQueryFnData>({
queryKey,
queryFn,
options,
}: Query<TQueryFnData>) => {
const result = useQueryOrigin<TQueryFnData, ExceptionBase>({
queryKey,
queryFn,
...options,
});
return result;
};
export default useQuery;
이로써 React-Query의 useQuery Hook에 대한 의존성을 조금이나마 줄였습니다.
새로 정의한 useQuery Hook 내 공통 로직을 추가함으로써 중복 코드를 줄일 수 있는 이점도 있습니다.
저는 React-Query의 useMutation Hook을 이와 같이 재정의하고, 내부에 동일한 API 요청을 1초에 한 번만 실행하는 공통 로직을 추가하여 버튼 연타시 발생하는 문제를 해결하기도 했습니다.
localStorage Web API 의존성 낮추기
window.localStorage 이용에서 최종적으로 어떤 기능이 필요한지 추상적으로 정의해봅시다.
- 특정 키에 저장된 데이터 가져오기 - get
- 특정 키에 데이터 저장하기 - set
- 특정 키의 데이터 삭제하기 - remove
이제 정의한 기능들을 구현한 인터페이스를 생성해봅시다.
export default class LocalStorage<Data = unknown> {
private key;
constructor(key: string) {
this.key = key;
}
get() {
const data = window.localStorage.getItem(this.key);
if (data === null) {
return data;
}
return JSON.parse(data) as Data;
}
set(data: Data) {
window.localStorage.setItem(this.key, JSON.stringify(data));
}
remove() {
window.localStorage.removeItem(this.key);
}
}
마찬가지로 별도의 localStorage 인터페이스를 생성함으로써 window.localStorage에 대한 의존성을 줄일 수 있습니다. 변경에 대한 대응은 생성한 LocalStorage 클래스에서만 수행하면 될테니까요.
이 뿐만 아니라 클래스를 통해 키(key)에 대한 은닉화도 할 수 있겠네요.
위 LocalStorage 클래스는 아래와 같이 활용할 수 있습니다.
import type { Book } from 'book';
import LocalStorage from '../../lib/LocalStorage';
class NewBookRepository extends LocalStorage<Book> {
constructor() {
super('SEJULBOOK_NEWBOOK');
}
}
export default NewBookRepository;
이렇게 하면 로컬 스토리지에 대한 키가 분산되지 않고, 키를 몰라도 특정 데이터에 접근할 수 있습니다.
또, 만약 위 NewBookRepository 클래스 내 프로퍼티 또는 메서드를 추가하여 더 확장성 있게 사용할 수도 있겠네요.
.
.
.
우리의 프로젝트에는 정말 많은 라이브러리와 Web API가 사용됩니다. 이 모든 것들에 대해 위 예시들처럼 별도의 인터페이스를 구현할 수는 없겠죠.
따라서 여러 모듈들에서 빈번히 직접 접근하여 사용하는 것들을 선정하여 의존성을 줄이는 것이 좋아보입니다.
그리고 꼭 의존성을 줄이기 위해서가 아니더라도 확장성 및 재사용성을 높이기 위한 목적으로도 좋은 방법이 될 수 있습니다.