React에서 관심사를 분리하는 법 (feat . customHook, router )

2023. 5. 21. 17:22프론트엔드

Clean Code

  • 좋은 코드를 쓰는것은 중요하다. → 나쁜 코드를 쓰지 않는 것이 중요하다.
  • 하지만 과거의 코드( Legucy )가 나중에 봤을 때 나쁜코드를 의미하는 것은 아니다.
  • 그 당시에 잘 짜인 코드는 지금와서도 마이그레이션 하기 쉬운 코드다. 비판적인 시선으로 바라보지 말자.
  • 좋은 코드를 작성하기 위해서는 좋은 코드의 기준이 무엇인지, 그리고 방법이 무엇인지를 고민하고 연구해야 합니다.

관심사의 분리

개발에는 관심사의 분리 ( Seperation of Concerns ) 용어가 있다.

좋은 코드를 짜기 위한 가장 기본적인 원칙이며, 더 좋은 애플리케이션을 만들기 위한 여러 디자인 패턴, 기법, 아키텍쳐 등은 결국 모두 이 SoC를 가장 기본적인 원칙으로 삼고 있다.

😒 관심사란 ?

  • 하나의 모듈이 수행하고자 하는 목적 → 모듈 : 함수, 클래스 등의 단위로 해석 가능
  • 관심사의 분리 : 모듈들이 한번에 여러 관심사를 처리하려고 하지 말고, 하나의 관심사만 처리하도록 분리하는 것을 의미함.

관심사를 분리하는 이유

⚠️ 왜 관심사를 분리해야 하는걸까?

  • 관심사를 분리하면 하나의 모듈을 하나의 목적만 가지게 된다 → 이 코드가 수정될 이유는 한가지만 존재하게 된다는 것
  • 좋은 소프트웨어에서는 기존의 기능을 수정하는 것과 기능을 확장하는 것을 잘 할 수 있어야 한다. 이를 우리는 유지보수라고 부른다.
    • ex) 인증&인가에 대해서 모든 모듈들이 관여하고 있다면, 추후 인증 & 인가의 동작을 수정해야 할 경우
    • 인증&인가를 다루는 핵심 모듈을 한가지로 제한 (MainRoutes) → 나머지는 이 모듈을 사용하는 형식으로설계
    • MainRoutes를 수정하면 모든 모듈들이 수정 되기 때문에 변화에 유연하게 대응 가능
  • SRP : 단일 책임 원칙 (single responsibility principle )
    • 모든 모듈들은 책임을 하나만 가져야 한다. ( 관심사랑 동일한 뜻 )
  • KISS : Keep It Simple Stupid
    • 각 모듈들은 간단하고, 단순하게 만들라는 의미

Custom Hook

React에서 컴포넌트를 선언하는 방법의 대세 Class → Function형으로 옮겨 졌다.

  • 함수 컴포넌트의 문법이 더 단순하고, 교착상태로 인한 버그가 발생하지 않는다.
  • CustomHook 의 편리함과 유용성도 큰 비중을 차지하고 있다. ( 멘토님 Think )

React의 관심사

리액트가 가진 관심사 ?

  1. UI ( 컴포넌트 ) : 실세 코드상에서는 jsx라는 형태로 표현
  2. 로직 ( UI를 변경시키는 부분 ) :
    • 유저의 입력에 반응하고, API를 호출하고, 스크린의 변화에 반응
    • 동작을 통해 UI에 영향을 미치는 행위

위 두가지로 볼 수 있다.

React의 관심사를 분리하는 법 : Presentational - Container

컴포넌트를 크게 두 계층으로 분리하는 방법

  • container: 로직들을 다루는 부분으로 UI에는 관여 하지 않고, 오로지 변화 하기 위한 로직에만 집중하는 컴포넌트
  • Presentaional: 반대로 로직은 상관하지 않고 UI가 어떻게 구성되어야 하는지에만 집중하는 컴포넌트

Hook 등장 전 까지 관심사를 분리하는 표준 패턴으로 사용되었다.

React에서 관심사를 분리하는 법: Cunstom Hook

커스텀 훅은 리액트가 기본적으로 제공해주는 훅들을 이용해서 만든 함수

  • 로직은 UI를 변경시키기 위함이고, 함수형 컴포넌트에서 로직은 대부분 useState, useEffect등의 Hook을 통해서 구현된다,
  • 훅을 통해서 편리하게 state를 선언하고, effect를 발생시킬 수 있게 되었지만,
  • 컴포넌트 내부에 많은 로직들이 들어가게 되면 컴포넌트가 복잡해지고, 무엇보다 동일한 로직들을 여러 컴포넌트에 걸쳐서 재사용하기 힘들다는 단점이 있다. ⇒ 유지보수가 힘들어진다!
  • 리액트에서도 Hook들을 이용한 동일한 로직들을 별도의 함수로 추출커스텀 훅

커스텀 훅의 조건은 아래와 같다.

  1. React의 Hook을 호출하는 함수여야 한다.
  2. 함수의 이름은 use로 시작해야 한다.
  • 순서를 지켜야 한다 & 제일 최상위 코드 블럭에서만 호출이 되어야 한다. & 조건문 내부 x
  • 컴포넌트나 다른 커스텀 훅 내부에서만 호출되어야 한다. ( 일반 함수안에서 호출되면 x )
  • 참고자료

Reusing Logic with Custom Hooks – React

Q. 컴포넌트 내부에서 로직과 관련된 코드가 길어지는 경우

재사용되지 않는 로직이더라도 커스텀 훅으로 분리하는게 권장되는 편인가요? 컴포넌트는 최대한 순수함수에 가까운 코드로 유지하는 것이 좋은지 궁금합니다.

→ 캐바캐 인데 , 분리를 해서 관리하면 유지보수에 조금 더 도움이 될것 같다.

  • 추상화: 핵심적인 내용만 남기고, 나머지는 발라낸다. ( 관심사에 따라 달라짐 )_
  • 핵심적인 내용이 무엇인지 결정을 해야한다. 나머지를 숨기는 것이다.

횡단 관심사

관심사 : 하나의 모듈이 수행해야하는 목적!

횡단 관심사 : 여러서비스에 걸쳐서 동작해야 하는 코드를 의미함.

⇒ 핵심적인 기능이아닌 중간중간 삽입되어야 하는 코드를 의미한다!

애플리케이션 내 여러 핵심 비즈니스 로직들에 걸쳐서 실행되어야 하는 동작들을 의미함

횡단 관심사의 대표적인 예시

  • 인증 & 인가
  • 로깅
  • 트랜잭션 처리
  • 에러처리

API 통신에서 횡단관심사 분리 하기

import axios from 'axios';
import { BASE_URL } from 'constant/config';
import { getUserTokenInLocalStorage } from 'utils/localTokenUtils';

export const axiosInstance = axios.create({
  baseURL: BASE_URL,
  headers: { 'Content-Type': 'application/json' },
});

export const axiosAuthInstance = axios.create({
  baseURL: BASE_URL,
  headers: { 'Content-Type': 'application/json' },
});

axiosAuthInstance.interceptors.request.use(
  config => {
    const token = getUserTokenInLocalStorage();
    const configCopy = { ...config };
    configCopy.headers = { ...config.headers };
    configCopy.headers.Authorization = `Bearer ${token}`;
    return configCopy;
  },
  error => Promise.reject(error),
);
  1. header에 Authorization 인증 Token 추가 ( Axios 인터셉터 이용 )
  2. BASE_URL 설정
import { AxiosError } from 'axios';
import { axiosAuthInstance } from './axiosInstance';

const todoApi = {
  fetchList: async () => {
    try {
      const response = await axiosAuthInstance.get('/todos');
      return response.data;
    } catch (error) {
      if (error instanceof AxiosError) {
        return error.response;
      }
    }
  },

  createTodo: async todoInfo => {
    try {
      const response = await axiosAuthInstance.post('/todos', todoInfo);
      return response.data;
    } catch (error) {
      if (error instanceof AxiosError) {
        return error.response;
      }
    }
  },

  updateTodo: async todoInfo => {
    try {
      const response = await axiosAuthInstance.put(
        `/todos/${todoInfo.id}`,
        todoInfo,
      );
      return response.data;
    } catch (error) {
      if (error instanceof AxiosError) {
        return error.response;
      }
    }
  },

  deleteTodo: async todoId => {
    try {
      const response = await axiosAuthInstance.delete(`/todos/${todoId}`);
      return response;
    } catch (error) {
      if (error instanceof AxiosError) {
        return error.response;
      }
    }
  },
};

export default todoApi;
  • 이후 동일한 Axios instance를 이용해서 구체화
  • 여기서도 에러 처리의 횡단 관심사를 분리할 수 있는 리팩토링을 할 수 있겠다.

인증인가 횡단관심사 분리하기

수정하기 전 코드

const router = createBrowserRouter([
  {
    path: '/',
    element: <Navigate to="/signin" />,
    errorElement: <NotFound />,
  },
  { path: '/signin', element: <SignIn /> },
  { path: '/signup', element: <SignUp /> },
  {
    path: '/todo',
    element: (
      <ProtectedRoute>
        <Todo />
      </ProtectedRoute>
    ),
  },
    {
    path: '/mypage',
    element: (
      <ProtectedRoute>
        <MyPage/>
      </ProtectedRoute>
    ),
  },
]);

export default function Router() {
  return <RouterProvider router={router} />;
}
  • router 객체: 모든 page의 Path와 Element정의한 객체 이 객체만 수정하면 모든 라우팅을 책임지는 객체를 한번에 수정할 수 있다.
  • 하지만 이를 좀 더 유지보수가 쉬운 코드로 바꾸는 방법이있다. 바로 횡단 관심사를 분리하는 것이다.

수정 후 코드

const RouteElements: routeElement[] = [
  {
    path: "/",
    element: <Main />,
    withAuth: false,
  },
  {
    path: "/signin",
    element: <SignIn />,
    withAuth: false,
    redirectPath: "/todo",
  },
  {
    path: "/signup",
    element: <SignUp />,
    withAuth: false,
    redirectPath: "/todo",
  },
  {
    path: "/todo",
    element: <Todos />,
    withAuth: true,
    redirectPath: "/signin",
  },
    {
    path: "/mypage",
    element: <MyPage />,
    withAuth: true,
    redirectPath: "/signin",
  },
];

const router = createBrowserRouter(
  RouteElements.map((route) => {
    if (route.redirectPath) {
      return {
        path: route.path,
        element: (
          <AuthLayout to={route.redirectPath} withAuth={route.withAuth}>
            {route.element}
          </AuthLayout>
        ),
        errorElement: <Error />,
      };
    }

    return {
      path: route.path,
      element: route.element,
      errorElement: <Error />,
    };
  })
);
  • 인증 인가 컴포넌트의 의존성을 빼고, 객체 속성인 withAuth를 활용해서 HOC를 덮어준다. 이로써 HOC를 덮어주는 횡단 관심사를 분리 했다.
  • 하나의 객체(RouteElements)로 인증인가 처리를 할 수 있고, 이는 나중에 사이드바나 nav에 추가로 사용될 수 있어 확장에 열려있는 코드라고 볼 수 있다.