금융 상품 추천 서비스 (Feat. React, TypeScript)

2023. 3. 5. 18:04기록/Projects

    목차

배포 링크 : credit-market (현재 서버가 닫혀서 기능 작동X)
원본 저장소: Github
전체 시연 영상: Youtube

이번에 하게 된 프로젝트는 금융 상품 추천 서비스 !!! 🐰🐰⭐️⭐️⭐️⭐️⭐️

금융 상품에는 카드, 대출 등의 선택지가 있었는데, 우리 팀은 대출 상품으로 정했다.

상품 데이터는 백엔드에서 api 를 이용해 데이터를 저장한 후에 사용하게 되었다.

 

이번 프로젝트에서도 내가 팀장을 맡게 되어서, 프로젝트 환경 셋팅을 진행했다.

지금까지는 CRA 로 리액트 앱 초기 설정을 했었는데,

이번에는 처음으로 vite 를 이용해 리액트 + 타입스크립트 프로젝트 셋팅을 해보았다.

 

아래의 항목들을 미리 반영해 두어서 팀원들이 사용하기 편하도록 했다.

  • axios 인스턴스 생성
  • 유틸 함수 생성
  • prettier & eslint 설정
  • vite 절대경로 설정
  • 상수 파일 생성
  • GlobalStyle 설정

 

나중에도 참고하기 위해서 설정 방법들을 작성해보겠당 !!

아래의 코드는 실제 코드에서 생략된 부분들이 있기 때문에

원본 코드를 보시려면 위 깃허브 저장소를 참고 부탁드립니다! ❤️


vite 를 이용해 React / TypeScript 프로젝트 생성

npm create vite@latest [프로젝트 이름] --template react-ts 

✔ Select a framework: › React 
✔ Select a variant: › TypeScript

 

vite 절대경로 설정

npm install vite-tsconfig-paths -D

 

vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import * as path from 'path';

export default defineConfig({
  plugins: [react()],
  mode: 'development',
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 3000,
  },
});

 

tsconfig.json

{ 
  "compilerOptions": { 
    "baseUrl": ".", 
    "paths": { 
      "@/*": ["src/*"] 
    }, 
  }, 
}

 

emotion 설치

npm install @emotion/react @emotion/styled emotion-reset

 

GlobalStyle 설정

import { css, Global } from '@emotion/react'; 
import reset from 'emotion-reset'; 

const GlobalStyle = () => { 
  return <Global styles={style} />; 
}; 

export default GlobalStyle; 

const style = css` 
  ${reset} 
  
  body { 
    // 
  } 
`;

 

App.ts

import GlobalStyle from './styles/GlobalStyle'; 

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 
  <Provider store={store}> 
    <GlobalStyle /> 
    <App /> 
  </Provider>, 
);

 

axios 설치

npm install axios

 

axios instance 생성

import { getCookie } from '@/utils/cookie';
import axios from 'axios';

const API_BASE_URL: string = import.meta.env.VITE_BASE_URL;

const axiosApi = (url: string) => {
  const instance = axios.create({ baseURL: url });
  instance.defaults.timeout = 3000;

  instance.interceptors.response.use(
    (response) => {
      return response.data;
    },
    (error) => {
      return Promise.reject(error);
    },
  );

  instance.interceptors.request.use(
    (config) => {
      const token = getCookie('accessToken');
      if (token) config.headers['Authorization'] = `Bearer ${token}`;
      return config;
    },
    (error) => {
      return Promise.reject(error);
    },
  );

  return instance;
};

export const axiosInstance = axiosApi(API_BASE_URL);

 

eslint & prettiier 설치

npm install eslint prettier -D
npm install eslint-config-prettier eslint-plugin-prettier -D

 

  • eslint-config-prettier: eslint와 prettier간 충돌나는 규칙을 비활성화해주는 eslint 설정
  • eslint-plugin-prettier: prettier 규칙을 생성하는 eslint 플러그인

 

타입스크립트 관련 플러그인 설치

npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser -D

 

 

리액트 관련 플러그인 설치

npm install eslint-plugin-react eslint-plugin-react-hooks -D

 

eslint 초기 설정

npx eslint --init

 

.eslintrc.json

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "overrides": [],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "plugins": ["react", "@typescript-eslint"],
  "rules": {}
}

 

.prettierrc.json

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100
}

 

라우팅 초기 설정

Router.tsx

import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { ROUTES } from '@/constants/routes';

...

const Router = () => {
  return (
    <Layout>
      <Routes>
        <Route path={ROUTES.HOME} element={<Home />} />
        <Route path={ROUTES.SIGNUP} element={<Signup />} />
        <Route path={ROUTES.LOGIN} element={<Login />} />
        <Route path={ROUTES.SEARCH} element={<Search />} />
        <Route path={ROUTES.PRODUCT_DETAIL} element={<ProductDetail />} />
        <Route
          path={ROUTES.MYPAGE}
          element={
            <PrivateRoute>
              <Mypage />
            </PrivateRoute>
          }
        />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Layout>
  );
};

export default Router;

 

경로 상수 파일 작성

export const ROUTES = {
  HOME: '/',
  SIGNUP: '/signup',
  LOGIN: '/login',
  WELCOME: '/signup/welcome',
  SEARCH: '/search',
  CART: '/cart',
  BUY: '/buy',
  MYPAGE: '/mypage',
  MYPAGE_FAVOR: '/mypage/favor',
  MYPAGE_BUY: '/mypage/buy',
  MYPAGE_INFO: '/mypage/info',
  PRODUCTS: '/products',
  PRODUCT_DETAIL: '/products/:id',
  PRODUCT_BY_ID: (id: string) => `/products/${id}`,
};

 

api 호출 경로 상수 파일 작성

export const API_URLS = {
  SIGNUP: '/usersignup',
  LOGIN: '/userlogin',
  LOGOUT: '/userlogout',
  CART: '/cart',
  ORDER: '/order',
  DETAIL: (id: string) => `/item/${id}`,
  FAVOR: (id: string) => `/favor/${id}`,
  BUY: (id: string) => `/buy/${id}`,
  BUY_LIST: (page: number) => `/buy/${page}`,
  FAVOR_LIST: (page: number) => `/favor/${page}`,
  ...
};

 

모달창 출력 메시지 상수 파일 작성

export const MESSAGES = {
  CART: {
    ERROR_GET: '장바구니 조회 중\n에러가 발생하였습니다.',
    ERROR_CREATE: '장바구니 추가 중\n에러가 발생하였습니다.',
    ERROR_DELETE: '장바구니 삭제 중\n에러가 발생하였습니다.',
    COMPLETE_ADD: '장바구니에 추가되었습니다.',
    ERROR_DUPL: '이미 장바구니에 담은 상품입니다.',
    CHECK_DELETE: '선택하신 상품을\n삭제하시겠습니까?',
    COMPLETE_DELETE: '삭제가 완료되었습니다.',
    ERROR_NOT_CHECK: '선택하신 상품이 없습니다.',
  },
  ...
  INVALID_AUTH: '회원 전용 메뉴입니다.\n로그인 후 이용해 주세요.',
};

 

색상 상수 파일 생성

const COLORS = {
  primary: '#19418A',
  secondary: '#000540',
  white: '#fff',
  black: '#000',
  background: '#f9fbff',
  ...
};

export default COLORS;

 

redux toolkit store 파일 생성

import { configureStore } from '@reduxjs/toolkit';
import cart from './cartSlice';
import loading from './loadingSlice';

export const store = configureStore({
  reducer: {
    loading: loading.reducer,
    cart: cart.reducer,
    ...
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }),
  devTools: true,
});

export type RootState = ReturnType<typeof store.getState>;

export default store;

 

+ .github 하위 경로에 Issue / PR 템플릿 생성


나의 구현 파트 시연 움짤들을 기록해 보겠다!

내 파트는 이렇게 세 부분이었는데, 나중 갈수록 더 추가되긴 했다....ㅎ

  • 상세 페이지
  • 장바구니
  • 신청 페이지

 

장바구니 추가/삭제

상품 상세 페이지에서 장바구니 버튼을 클릭하면 장바구니에 추가된다.

만약 이미 장바구니에 있는 상품일 경우, 이미 담겨있다는 모달창을 보여준다.

 

전체삭제 및 선택 삭제가 가능하며

담은 상품이 없을 경우 장바구니 로티를 보여준다.


상품 신청하기

장바구니에서 상품 선택 후 신청하기 버튼을 클릭하면 상품 신청 페이지로 이동한다.

 

필수 동의에 체크한 후 신청완료 버튼을 클릭하면 비밀번호 인증 후 신청이 완료된다.

만약 비밀번호 인증에 실패할 경우 비밀번호 확인 모달창이 출력된다.

 

신청이 완료되면 마이페이지의 신청 상품 목록으로 이동하여 방금 신청한 상품을 보여준다.


관심상품 추가/삭제

상품 상세 페이지 또는 상품 목록에서 하트 버튼을 클릭하면 관심 상품으로 추가된다.

다시 하트 버튼을 누르면 관심 상품이 해제된다.

 

마이페이지의 관심 상품 목록에서 확인이 가능하다.


타입스크립트를 제대로 적용해본 프로젝트는 처음이었는데,

인터페이스를 공통으로 사용하면서 그래도 많이 익숙해진 것 같다.

 

또한 백엔드 분들과 api 응답 값에 대해 이야기 나눌 때,

타입스크립트 인터페이스를 고려하여 소통해야 한다는 것을 새로 알게 되었다!

 

팀원분들 너무너무 고생 많으셨습니다! 🐶❤️❤️❤️

이제 파이널 프로젝트가 남았당....

앞으로도 열심히 해보자꾸 !!!!!!!!!!!