투두리스트 과제 후기

Daisy🌼·2023년 1월 22일
1

원티드 프리온보딩 인턴십을 참가하면서 선발 과제 포함 총 4개의 기업 과제를 수행했습니다. 과정을 진행하면서 스스로 많이 부족하다고 느꼈고, 특히 다른 사람들의 코드를 보면서 이해하는 능력이 필요하다고 생각했습니다. 그래서 수료 이후 코스에 참여한 12팀의 코드를 모두 살펴보면서 배울 점을 기록하려고 합니다.

📝 과제 내용

  • 로그인, 회원 가입 기능이 포함되어 있는 투두리스트를 만드는 과제입니다. 해당 과제는 선발 과제로 공개되어 있기 때문에 자세한 요구사항은 링크에서 확인할 수 있습니다. (선발 과제 링크)

📝 분석 기준

  • 과제의 요구 사항과 별개로, 12팀의 모든 코드를 분석할 때 중점적으로 확인할 기준이 필요했습니다. 저는 스스로 과제를 수행하면서 고민했던 부분을 위주로 기준을 삼았습니다.

1. 프라이빗 라우트

  • 저는 과제의 요구사항을 만족하기 위해 액세스 토큰의 유무에 따라 특정 경로로 이동시키는 로직을 useEffect에서 관리하곤 했습니다.
useEffect(()=>{
	const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
  	if(!토큰) navigate('/');
});
  • 하지만 이는 좋지 않은 코드라는 것을 알게 되었습니다. useEffect의 콜백은 렌더링된 이후에 실행되기 때문에 권한이 필요한 페이지가 보여진 다음에 로그인 유무를 확인하는 것은 보안상 적절하지 않습니다.

  • 따라서 상위의 라우터 단계에서 먼저 권한 유무를 확인하고 페이지를 적절히 이동시키는 것이 합리적이라고 판단했습니다. 그래서 저는 다른 팀들은 어떻게 라우트를 생성했는지 궁금했습니다.

2. Axios 활용

  • Axios 라이브러리는 HTTP 통신과 관련된 유용한 기능과 설정을 제공합니다. 그동안 저는 baseURL을 설정하는 정도로 사용했는데, 실제로는 더욱 다양한 커스텀 설정을 사용할 수 있기 때문에 이를 활용하는 코드를 배우고 싶었습니다.

📝 프라이빗 라우트

  • 프라이빗 라우트 (Protected Routes라고도 많이 합니다.)는 조건을 만족하는 경우에만 권한이 있는 페이지에 접근하도록 하는 라우트입니다. 예를 들어 로그인 되어 있는 사용자만 접근이 가능하도록 만드는 것입니다.

📌 케이스 1

  • 우선 퍼블릭 라우트프라이빗 라우트를 생성하는 방법이 있었습니다.

  • 퍼블릭 라우트에서는 로컬스토리지에 토큰이 존재하면 투두리스트를 보여주는 페이지로 이동하고, 그렇지 않으면 children을 보여줍니다.

// Public Route
const PublicRoute = ({ children }) => {
  const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
  
  return 토큰 ? <Navigate to="/todo" /> : children;
};

export default PublicRoute;
  • 프라이빗 라우트에서는 토큰이 있으면 children을, 그렇지 않으면 로그인/회원가입 페이지로 이동합니다.
const PrivateRoute = ({ children }) => {
  const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
  return 토큰 ? children : <Navigate to="/" />;
};

export default PrivateRoute;
  • 이렇게 생성한 퍼블릭 라우트와 프라이빗 라우트 컴포넌트 내부에 실제 비즈니스 로직이 포함된 컴포넌트를 중첩해 감싸는 형태로 만들어줄 수 있습니다.
// App.js
function Router() {
  return (
    <Routes>
      <Route
        path="/"
        element={
          <PublicRoute>
            <Home />
          </PublicRoute>
        }
      />
      <Route
        path="/todo"
        element={
          <PrivateRoute>
            <Todos />
          </PrivateRoute>
        }
      />
    </Routes>
  );
}

export default Router;
  • 따라서 로그인 여부를 확인하는 일을 비즈니스 로직이 포함된 컴포넌트의 상위 컴포넌트인 퍼블릭 라우트와 프라이빗 라우트에 위임할 수 있습니다.

  • 또한 개인적으로 Public, Private라는 이름을 통해 로그인과 상관없이 접근 가능한 페이지와 로그인이 필요한 페이지가 명확하게 드러난다는 점에서 이러한 방식이 직관적으로 느껴졌습니다.

📌 케이스 2

  • 리액트 라우터에서 제공하는 기능을 적극적으로 활용한 사례도 있었습니다.

  • useLocation, Outlet 그리고 버전 6에서 새롭게 추가된 createBrowserRouter로 프라이빗 라우팅을 구현한 예시입니다.

  • 퍼블릭 라우터에서는 현재 페이지의 경로를 가져와 토튼이 있으면 원하는 페이지로 이동시키고, 토큰이 존재하지 않으면 자식 라우트의 요소를 보여줍니다. 이를 <Outlet /> 컴포넌트로 나타낼 수 있습니다.

const PublicRouter = () => {
  const { pathname } = useLocation();
  const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();

  if (pathname === '/') {
    return 토큰 ? <Navigate to='/todo' replace /> : <Outlet />;
  }
  return 토큰 ? <Navigate to='/' replace /> : <Outlet />;
};

export default PublicRouter;
  • 프라이빗 라우터의 경우, 토큰이 없으면 루트 경로로 이동시키고 토큰이 있으면 자식 라우트 요소를 보여줍니다.
const PrivateRouter = () => {
	const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();

  return !토큰 ? <Navigate to='/' replace /> : <Outlet />;
};

export default PrivateRouter;
  • 그리고createBrowserRouter를 통해 라우트를 구성할 수 있습니다.
export const router = createBrowserRouter([
  {
    element: <PublicRouter />,
    children: [
      {
        path: '/'
        element: (
            <AccountPage />
        ),
      },
    ],
  },
  {
    element: <PrivateRouter />,
    children: [
      {
        path: '/todo'
        element: (
            <TodoPage />
        ),
      },
    ],
  },
]);
  • 앞서 살펴본 케이스와 유사하게 퍼블릭, 프라이빗이라는 직관적인 라우트를 생성해 비즈니스 로직이 포함되어 있는 컴포넌트를 children으로 받고 있습니다. 이를 통해 로그인 여부를 확인하는 로직은 비즈니스 로직의 상위 컨텍스트의 라우트로 구현할 수 있다는 점을 알 수 있었습니다.

💡 참고할 만한 문서


📝 Axios 활용

  • 대부분 Axios 라이브러리를 사용해 HTTP 요청과 관련된 로직을 처리하고 있었습니다.

  • 이번 프로젝트의 경우, 인스턴스를 생성하고 인터셉터를 추가해 요청을 보내기 전에 액세스 토큰의 유무를 검사해 있을 경우에만 요청 헤더에 액세스 토큰을 넣는 코드가 많았습니다.

📌 케이스 1

// 요청을 보내는 루트 URL을 미리 설정하여 인스턴스를 생성
const client = axios.create({
  baseURL: process.env.REACT_API_URL,
});
// 인터셉터로 토큰 유무에 따라 요청 헤더를 다르게 추가하는 로직
client.interceptors.request.use((config) => {
  const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
  
  if (!토큰) {
    config.headers['Content-Type'] = 'application/json';
    return config;
  }
  
if (토큰 && config.headers) {
    config.headers.Authorization = 토큰;
    config.headers['Content-Type'] = 'application/json';
    return config;
  }
});

export default client;

📌 케이스 2

// 인터셉터
apiClient.interceptors.request.use(
  (config) => {
    const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
    if (토큰) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    } else {
      delete config.headers.Authorization;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

📌 케이스 3

// 인터셉터 
axiosInstance.interceptors.request.use(config => {
  const 토큰 = 로컬스토리지에서_토큰을_불러오는_함수();

  if (!토큰) {
    config.headers.Authorization = null;
  } else {
    config.headers.Authorization = `Bearer ${토큰}`;
  }

  return config;
});

📌 케이스 4

  • null 병합 할당을 사용해 요청 헤더를 설정한 케이스도 있었습니다.null 병합 연산자를 할당 연산자와 함께 쓰면 nullundefined인 경우에 값을 할당합니다. 투두리스트 프로젝트에서는 요청 헤더의 Authorization에 값이 없을 경우 액세스토큰을 넣어 인터셉터의 설정을 추가할 수 있습니다.
axiosInstance.interceptors.request.use((config) => {
	setAccessToken();

  return config;
});

function setAccessToken(config) {
  if (!config.headers) {
    return config;
  }
  
  const bearerToken = `Bearer ${로컬스토리지에서_토큰을_가져오는_함수()}`;
  config.headers.Authorization ??= bearerToken;
}
profile
커피와 재즈를 좋아하는 코린이 | 좋은 글 좋은 코드를 쓰고 싶습니다

0개의 댓글