[1회차] 로그인 페이지 - 좋은 ‘모듈' 설계하고 구현해보기
- 로그인을 위해 필요한 Data Fetching 모듈을 만들어볼 것입니다.
- 주어진 Interface에 맞게 Class 및 Function들을 만들어보며 지속가능한 모듈 설계와 프론트엔드에서의 객체지향에 대해서 고민해봅니다.
- 3번 이상 반복하지 않는다.
- 관심사를 분리한다.
- 코드를 가독성 좋게 작성한다.
아직 많은 경험이 많지 않아서 리팩토링을 한다고하면 어디부터 해야될지 좀 막막했다. 리팩토링에 대한 글들을 보면서 내가 지키고 싶은 기준을 3가지 추렸고 주어진 코드를 기준에 맞춰 생각했다.
import axios from "axios";
import cookies from "js-cookie";
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 });
}
...
}
export default new AuthService();
세가지를 나눠서 하나의 파일이 하나의 역할을 담당하도록 수정하고싶다.
미션가이드에서는 하나의 부모클래스를 상속하는 방식을 권하셨다.
공통적으로 사용될 부분은 axios라고 생각했고 axios instance를 갖고있는 HttpClient 클래스를 만들어서 부모클래스로 지정해야겠다고 계획했다.
관심사 분리를 위해 파일을 3개로 나눴습니다.
class AuthService extends HttpClient {
constructor() {
/** '/auth'를 baseURL에 전달해서 반복작성 피함 */
super('/auth');
}
async refresh() {
const { data } = await this.client.post('/refresh');
setToken(data);
}
...
}
외부 라이브러리 axios, cookie와 분리하여 인증 관련 비즈니스 로직에만 집중하는 클래스로 만들었습니다.
const url = process.env.NEXT_PUBLIC_API_HOST;
export default class HttpClient {
client: AxiosInstance;
/** 유니온 타입으로 이후 추가될 path 자동완성 만들어줌 */
constructor(path: '/auth' | '/users') {
this.client = axios.create({
baseURL: url + path,
withCredentials: true,
});
/** 토큰이 있다면 헤더에 추가해줌 */
this.client.interceptors.request.use((config) => {
const token = getRefreshToken;
if (token && config.headers) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
}
}
네트워크 요청, axios에 관련된 내용은 전부 여기에 정리했습니다. 이후 요청 결과에 대한 핸들러도 여기서 독립적으로 관리할 수 있습니다.
이후에 추가될 클래스에 공통적으로 상속해줄 클래스이기 때문에 공통적으로 사용할 코드만 골랐습니다.
(setToken함수는 AuthService에선 반복해서 사용하지만 다른 클래스에선 쓸 일이 없을 것이라 판단해서 따로 뺐습니다.)
import cookies from 'js-cookie';
type Token = {
access: string;
refresh: string;
};
export const getRefreshToken = () => cookies.get('refreshToken');
export const getAccessToken = () => cookies.get('accessToken');
export const setToken = (token: Token) => {
cookies.set('accessToken', token.access, { expires: 1 });
cookies.set('refreshToken', token.refresh, { expires: 7 });
};
js-cookie 라이브러리를 통해 토큰을 저장하고 읽기만 하는 역할을 담당합니다.
의존성 역전 원칙
고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.
고수준 모듈: 프로그램의 의미 있는 단일 기능을 수행
저수준 모듈: 단일 기능을 위해 구현되어야 할 좀 더 구체적인 하위 기능을 수행문제상황: 고수준 모듈이 저수준 모듈에 의존할 경우, 세부내용을 수정하려다 기능 전반에 원치 않은 수정을 동반하게 된다.
해결방법: 작은 기능을 갈아끼워 사용할 수 있도록 추상화한다.
import { useQuery } from "react-query";
import { UserService } from "../src/services";
const Home: NextPage = () => {
const { data: me } = useQuery("me", UserService.me, {
refetchInterval: 500,
});
// ...
}
개인적으로 외부 라이브러리를 여기저기서 import해서 퍼트려놓는 걸 조심하는 편이다.
한 곳에서 정의하고 관리하는 것이 유지보수에 좋다고 생각한다.
의존성 역전원칙을 적용해서 useQuery가 하위 모듈에 의존하는 것이 아니라 하위 모듈들이 추상화된 모듈인 useRequest에 의존하도록 만든다.
지금은 api 요청을 할 때마다 react-query 라이브러리와 service 클래스를 모두 임폴드 해야하는 구조이다.
useOOO 훅을 만들어서 hook만 임폴드해서 간편하게 사용하도록 리팩토링할 예정이다.
import { useMe } from '../src/hooks/auth.hooks';
const Home: NextPage = () => {
const { data: me } = useMe();
// ...
}
useMe 훅 하나만 임폴트해서 간단하게 API 요청을 보낼 수 있도록 했다.
import {
useQuery,
QueryKey,
QueryFunction,
UseQueryOptions,
} from 'react-query';
export const useRequest = (
key: QueryKey,
queryFn: QueryFunction,
options?: UseQueryOptions
) => useQuery(key, queryFn, options);
의존성 역전원칙을 react-query에 적용하기 위해 만들어진 훅이다. useMutation 등 다른 훅을 위해선 좀 더 고민이 필요함.
import { useRequest } from './useRequest';
import { UserService } from '../services';
export const useMe = () =>
useRequest('me', UserService.me, { refetchInterval: 500 });
export const useRead = (id: number) =>
useRequest(['read', id], () => UserService.read(id));
요청을 할 때 마다 어떤 클래스의 어떤 함수와 어떤 키를 사용해야하는지 기억할 필요없이 하나의 훅만 호출해서 사용할 수 있도록 만들었다.
user와 관련한 요청을 한 곳에서 관리할 수 있다.
타입스크립트로 프로젝트를 진행해본 것은 처음 이어서 react-query에서 필요한 타입을 얻어올 때 디테일이 모자랐던 것 같다.
의존성 역전원칙이 하위모듈에 의존하지 않고 갈아끼워 사용할 수 있도록 공통된, 추상화된 모듈을 만들어 하위 모듈이 이것에 의존하도록 한다는 것 까지 이해는 했다. 근데 실제로 적용을 하려고 하니 좀 어려웠다.
결과적으론 똑같은 인자를 받아 똑같은 값을 그대로 내게 만들었는데 갈아끼우는덴 성공했지만 이게 유의미해보이진 않아서 다른 사람들의 해결방법이 궁금했다.
useRequest라는 훅이 요청에 관련된 훅인데 가이드라인만 보고는 어떤 요청까지 담당하는지 감이 안와서 일단 get만 적용을했는데 이러면 이름을 useFetch로 바꾸는게 더 맞을 것 같다고 생각했다.