react
ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY
pnpm error
vercel Build Failed
routing
index entry

프론트엔드 관심사 분리

프론트엔드 엔지니어링 코드의 관심사를 분리하는 방법들입니다. 모두 훔쳐배운 것들입니다.

vercel에서 경험한 이런저런 에러

일단 이것부터 시작하겠습니다.

vercel Build Failed

ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY  Broken lockfile: no entry for '/react/18.2.0' in pnpm-lock.yaml

어제 pnpm으로 설치했는데 vercel 배포환경에서 계속 에러가 발생했습니다. 왜 발생하고 누가 해결했는지 자료를 찾을 수 없었습니다.

해결: pnpm을 yarn으로 전환

학습: pnpm은 래퍼런스가 더 쌓였을 때 학습하고 활용합시다.

vercel 404 페이지

https://poylib.tistory.com/85

 "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }]

https://vercel.com/docs/concepts/projects/project-configuration#rewrites

관심사 분리하기

스타일링

인덱스 엔트리는 이렇게 작성할 수 있습니다.

export * from './flex';
export * from './position';
export * from './textStyle';

라우팅 처리

// App.tsx
import { useRoutes } from 'react-router-dom';
import { routes } from '@/routes/Routes';

export default function App() {
  const routedElements = useRoutes(routes);
  return <div>{routedElements}</div>;
}
// src/routes/Routes.tsx
import { lazy } from 'react';
import { ROUTE_PATHS } from '@/constants/config';
import GlobalLayout from './_globalLayout';

const SignUpPage = lazy(() => import('@/pages/SignUp'));
const SignInPage = lazy(() => import('@/pages/SignIn'));
const TodoPage = lazy(() => import('@/pages/Todo'));
const WelcomePage = lazy(() => import('@/pages/Welcome'));

export const routes = [
  {
    path: '/',
    element: <GlobalLayout />,
    children: [
      { index: true, element: <WelcomePage /> },
      { path: ROUTE_PATHS.signUp, element: <SignUpPage /> },
      { path: ROUTE_PATHS.signIn, element: <SignInPage /> },
      { path: ROUTE_PATHS.todo, element: <TodoPage /> },
    ],
  },
];

레이지 로딩하는 전략이 상당히 흥미롭습니다. 실제 필요한 페이지를 접근하기 전까지 import를 보류하고 있었습니다.

// src/routes/_globalLayout.tsx
import { Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import Loading from '@/components/Loading';
import { Navbar } from '@/components/Navbar';
import useCheckAuth from '@/hooks/useCheckAuth';

export default function Layout() {
  const isLoggedIn = useCheckAuth();
  return (
    <Suspense fallback={<Loading />}>
      <Navbar isLoggedIn={isLoggedIn} />
      <Outlet context={isLoggedIn} />
    </Suspense>
  );
}

로그인을 차러하기 전까지 Nav를 Suspense합니다. 내부도 또 Suspense 처리하는 전략입니다ㅏ.

src/router/Router.jsx / wanted-frontedend-team5

src/routes/Routes.tsx / WANTED-TEAM03

2개의 레포를 비교해보니까 createBrowserRouter를 사용하는 것이 베스트 프렉티스 같습니다.

스타일링은 개별 모듈로 분리하는 것이 베스트 프렉티스입니다.

스타일링 분리

https://github.com/wanted-frontedend-team5/pre-onboarding-10th-2-5/blob/main/src/components/Title/Title.styles.js

https://github.com/wanted-frontedend-team5/pre-onboarding-10th-2-5/tree/main/src/styles/utils

/Title
  Title.tsx
  Title.styles.tsx
  index.ts

스타일링과 마크업 그리고 export 모두 분리합니다.

export * from './Title';
// src/components/layouts/AppLayout/AppLayout.styles.js
import styled from 'styled-components';
import { APP_MAX_WIDTH } from 'styles/constants/dimensions';
import { flex } from 'styles/utils';

export const AppLayout = styled.div`
  ${flex({ justifyContent: 'center' })}
  min-height: 100vh;

  padding: 16px;
`;

export const Main = styled.main`
  width: 100%;
  max-width: ${APP_MAX_WIDTH}px;
`;
// src/components/layouts/AppLayout/AppLayout.jsx
import * as Styled from './AppLayout.styles';

export const AppLayout = ({ children }) => {
  return (
    <Styled.AppLayout>
      <Styled.Main>{children}</Styled.Main>
    </Styled.AppLayout>
  );
};

이런 패턴으로 사용합니다. 스타일링과 마크업을 분리하는 것은 좋습니다. 하지만 스타일이라는 접두어가 필요한지는 의문입니다. 저라면 그냥 import 했을 것입니다.

라우팅 constants

라우팅은 관심사를 묶어주시기 바랍니다.

export const BASE_URL = 'https://www.pre-onboarding-selection-task.shop';

export const API_URLS = {
  todos: '/todos',
  signIn: '/auth/signin',
  signUp: '/auth/signup',
};

export const ROUTE_PATHS = {
  welcome: '/',
  signIn: '/signin',
  signUp: '/signup',
  todo: '/todo',
};

상수의 프로퍼티도 어퍼케이스 작성했을 것이지만 일단 좋은 것 같습니다. 그리고 타입스크립트 답게 as const도 뒤에 추가할 것 같습니다. 진짜로 읽기 전용으로 만들 것 같습니다.

인터셉터 활용

https://github.com/wanted-frontedend-team5/pre-onboarding-10th-1-5/blob/main/src/api/axiosInstance.js

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

interceptors의 역할이 요청을 보내기 전에 전처리를 합니다. 토큰을 접근하고 기존 header, config 설정은 복사합니다.

axios 에러 객체

import { AxiosError } from 'axios';
import { axiosInstance } from './axiosInstance';

const signIn = async (signInData) => {
  try {
    const response = await axiosInstance.post('/auth/signin', signInData);
    return response;
  } catch (error) {
    if (error instanceof AxiosError) {
      return error.response;
    }
  }
};

const signUp = async (signUpData) => {
  try {
    const response = await axiosInstance.post('/auth/signup', signUpData);
    return response;
  } catch (error) {
    if (error instanceof AxiosError) {
      return error.response;
    }
  }
};

const authApi = {
  signIn,
  signUp,
};

export default authApi;

에러 객체를 제어할 때 axios의 타입을 활용하는 전략이 있었습니다.

타입가드 방식으로 해당하면 반환하도록 설정합니다. 특이 부분은 싱글튼의 메서드로 export하고 있습니다. axios를 제어하는 방식은 싱글튼 패턴이 일반적인듯 합니다.

https://github.com/WANTED-TEAM03/pre-onboarding-10th-1-3/tree/main/src/services

https://github.com/wanted-frontedend-team5/pre-onboarding-10th-1-5/tree/main/src/api