본문으로 건너뛰기

프론트엔드 관심사 분리

· 약 6분
arch-spatula

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

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