[1회차] 좋은 모듈 설계하고 구현해보기

유현지·2022년 6월 22일
1

Numble

목록 보기
1/3
post-thumbnail

[1회차] 로그인 페이지 - 좋은 ‘모듈' 설계하고 구현해보기

  • 로그인을 위해 필요한 Data Fetching 모듈을 만들어볼 것입니다.
  • 주어진 Interface에 맞게 Class 및 Function들을 만들어보며 지속가능한 모듈 설계와 프론트엔드에서의 객체지향에 대해서 고민해봅니다.

📝 리팩토링 기준

  • 3번 이상 반복하지 않는다.
  • 관심사를 분리한다.
  • 코드를 가독성 좋게 작성한다.

아직 많은 경험이 많지 않아서 리팩토링을 한다고하면 어디부터 해야될지 좀 막막했다. 리팩토링에 대한 글들을 보면서 내가 지키고 싶은 기준을 3가지 추렸고 주어진 코드를 기준에 맞춰 생각했다.




미션 1. AuthService 리팩토링 하기

  • AuthService는 인증 관련 비즈니스 로직들을 다루는 모듈
  • 이후에 추가될 다양한 Service 클래스들을 고려해서, 하나의 부모클래스를 extend 하는 방법으로 리팩토링



리팩토링 전 AuthService

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();

고쳐볼 곳 💭

관심사 분리

  • network 통신을 위한 axios
  • 토큰을 저장하고 읽는 cookie lib
  • 인증에 관련된 비즈니스로직을 모아둔 AuthService 클래스

세가지를 나눠서 하나의 파일이 하나의 역할을 담당하도록 수정하고싶다.

반복제거

  1. baseUrl을 매번 작성하는 것
  2. 토큰이 있다면 header에 토큰을 전달
  3. 성공하면 토큰을 access, refresh 에 각각 저장하는것

이후 추가될 클래스를 고려한 리팩토링

미션가이드에서는 하나의 부모클래스를 상속하는 방식을 권하셨다.
공통적으로 사용될 부분은 axios라고 생각했고 axios instance를 갖고있는 HttpClient 클래스를 만들어서 부모클래스로 지정해야겠다고 계획했다.


리팩토링 후

관심사 분리를 위해 파일을 3개로 나눴습니다.

1. AuthService.ts

class AuthService extends HttpClient {
  
  constructor() {
    /** '/auth'를 baseURL에 전달해서 반복작성 피함 */
    super('/auth');
  }

  async refresh() {
    const { data } = await this.client.post('/refresh');

    setToken(data);
  }

 ...
}

외부 라이브러리 axios, cookie와 분리하여 인증 관련 비즈니스 로직에만 집중하는 클래스로 만들었습니다.


2. api/ http.ts

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에선 반복해서 사용하지만 다른 클래스에선 쓸 일이 없을 것이라 판단해서 따로 뺐습니다.)


3. api/ token.ts

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 라이브러리를 통해 토큰을 저장하고 읽기만 하는 역할을 담당합니다.






미션 2. useRequest 리팩토링 하기

  • useRequest는 API request를 보내주는 모듈
  • react-query에 의존성 역전 원칙을 적용하기 위해 사용

의존성 역전 원칙

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

고수준 모듈: 프로그램의 의미 있는 단일 기능을 수행
저수준 모듈: 단일 기능을 위해 구현되어야 할 좀 더 구체적인 하위 기능을 수행

문제상황: 고수준 모듈이 저수준 모듈에 의존할 경우, 세부내용을 수정하려다 기능 전반에 원치 않은 수정을 동반하게 된다.

해결방법: 작은 기능을 갈아끼워 사용할 수 있도록 추상화한다.


리팩토링 전

page/ index.ts

import { useQuery } from "react-query";

import { UserService } from "../src/services";

const Home: NextPage = () => {
  const { data: me } = useQuery("me", UserService.me, {
    refetchInterval: 500,
  });
  
	// ...
}

고쳐볼 곳 💭

  1. 개인적으로 외부 라이브러리를 여기저기서 import해서 퍼트려놓는 걸 조심하는 편이다.
    한 곳에서 정의하고 관리하는 것이 유지보수에 좋다고 생각한다.

  2. 의존성 역전원칙을 적용해서 useQuery가 하위 모듈에 의존하는 것이 아니라 하위 모듈들이 추상화된 모듈인 useRequest에 의존하도록 만든다.

  3. 지금은 api 요청을 할 때마다 react-query 라이브러리와 service 클래스를 모두 임폴드 해야하는 구조이다.
    useOOO 훅을 만들어서 hook만 임폴드해서 간편하게 사용하도록 리팩토링할 예정이다.



리팩토링 후

1. page/ index.ts

import { useMe } from '../src/hooks/auth.hooks';

const Home: NextPage = () => {
  const { data: me } = useMe();

	// ...
}

useMe 훅 하나만 임폴트해서 간단하게 API 요청을 보낼 수 있도록 했다.


2. hook/ useRequest.ts

import {
  useQuery,
  QueryKey,
  QueryFunction,
  UseQueryOptions,
} from 'react-query';

export const useRequest = (
  key: QueryKey,
  queryFn: QueryFunction,
  options?: UseQueryOptions
) => useQuery(key, queryFn, options);

의존성 역전원칙을 react-query에 적용하기 위해 만들어진 훅이다. useMutation 등 다른 훅을 위해선 좀 더 고민이 필요함.


3. hook/ user.hooks.ts

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로 바꾸는게 더 맞을 것 같다고 생각했다.

다음번에 유의할 점

  • 타입스크립트에 대한 공부를 더 해야겠다. 특히 외부 라이브러리를 받아올 때 어떻게 타입을 구체화할지 공부해야겠다.
  • 커밋에 좀 더 신경쓰자 ! 깔끔하게 변경사항만 확인 할 수 있도록

0개의 댓글