[데브코스] WIL 14

devlog·2024년 6월 6일
0

풀뎁코

목록 보기
14/14
post-thumbnail

📍 이전에 node.js로 진행했던 book-store 프로젝트와 연동하기 위한 React 프로젝트 (1)
☑️ 사용 기술 스택
React + ts
zustand
styled components (sanitize)

Rreact 프로젝트 생성 방법

  • CRA
    • 초기 설정과 구성 자동화
    • webpack, babell
    • HMR - 코드 전체 build
  • vite
    • 빠른 속도와 효율
    • HMR - 모듈 단위 build

폴더 구조

  • pages
    • 라우트에 대응하는 페이지 컴포넌트(컨테이너)
  • components
    • 공통 컴포넌트, 각 페이지에서 사용되는 컴포넌트
  • utils
    • 유틸리티
  • hooks
    • 리액트 훅
  • model
    • 데이터 모델(타입)
  • api
    • api 연동을 위한 fetcher 등
    • 다른 파일과의 모호함을 해결하기 위해 user.model.ts의 형식으로 파일명 작성

React CLI

  • npm start
  • npm run build
    • build 폴더 생성
  • npm run test
    • test 파일 구동

typecheck

  • 타입스크립트의 타입 확인 스크립트 추가
  • noEmit
    • 출력 비활성화 ( 타입 확인만)
  • skipLibCheck
    • 외부 라이브러리 타입 체크 제외
  "scripts": {
    "typecheck": "tsc --noEmit --skipLibCheck"
  },

Global Style

  • 에릭마이어의 reset css
    • element 모두를 0으로 reset
  • nomalize.css
    • 각 element의 고유한 속성은 유지하며 브라우저의 차이만 줄임
  • sanitize.css
    • nomalize의 진보된 버전
    • where절 사용

css-in-js 필요 이유

  • 전역 충돌
  • 의존성 관리 어려움
  • 불필요한 코드, 오버라이딩
  • 압축
  • 상태 공유 어려움
  • 순서와 명시도
  • 캡슐화

theme

  • UI/UX 일관성 유지
  • 유지보수 용이
  • 확장성
  • 재사용성
  • 사용자 정의

WebStorage

sessionStorage

  • 브라우저가 열려있는 동안 제공 = 브라우저 또는 탭이 닫힐 때까지 데이터 저장
  • 데이터를 서버로 전송하지 않음
  • 저장 공간이 쿠키보다 큼

localStorage

  • 브라우저를 닫았다 열어도 데이터 잔존
  • 유효기간 없이 데이터를 저장하기 때문에 js를 사용하거나 브라우저 캐시, 또는 로컬 저장 데이터를 지워야만 사라짐
  • 저장 공간이 쿠키, sessionStorage보다 큼

기본 컴포넌트 작성

  • Title
  • Button
  • Input
  • Header & Footer
  • route
  • 모델 정의
  • API 통신
  • 회원가입 기능 구현 및 API 연동

Omit

  • 특정 속성만 제거한 타입 정의
    • stock만 제외, 여러개 제외할때 | 사용
interface Product {
  id: number;
  name: string;
  price: number;
  brand: string;
  stock: number;
}

type shoppingItem = Omit<Product, "stock">;

const apple: Omit<Product, "stock"> = {
  id: 1,
  name: "red apple",
  price: 1000,
  brand: "del"
};

test 파일

  • package.json에 추가하여 npm run test 파일명 으로 실행

"typecheck": "tsc --noEmit --skipLibCheck"

import { render, screen} from "@testing-library/react"
import Button from "./Button"
import { BookStoreThemeProvider } from "../../context/themeContext";

describe("Button 컴포넌트 테스트",()=>{
    it('렌더 확인', ()=> {
        // 1. 렌더시키고
        render(
            <BookStoreThemeProvider>
        <Button size="large" scheme="primary">버튼</Button>
        </BookStoreThemeProvider>
    );
        // 2. 확인
        expect(screen.getByText("버튼")).toBeInTheDocument();
        // 화면상에 버튼이라는 text를 가져와서 있는지 확인
    })
    
    it('size props 적용', ()=> {
        const {container} = render(
            <BookStoreThemeProvider>
            <Button size="large" scheme="primary">제목</Button>
            </BookStoreThemeProvider>
        )

        expect(screen.getByRole("button")).toHaveStyle({fontSize:"1.5rem"})
    }) // size large는 1.5rem
    
       
    it('color props 적용', ()=> {
        const {container} = render(
            <BookStoreThemeProvider>
            <Button size="large" scheme="primary">제목</Button>
            </BookStoreThemeProvider>
        )

        expect(screen.getByRole("button")).toHaveStyle({color:"white"})
    })  //scheme의 primary color는 white 
    
})

fowardRef

  • component element instance를 선택하는 레퍼런스를 자식에게 전달할 때
  • focus, target 등의 제어 상황에서 사용

라우트

npm i react-router-dom @types/react-router-dom --save
  • createBrowserRouter을 통해 router 생성
  • router를 레이아웃에 렌더 시키기 위해 RouterProvider 사용
import Layout from './components/layout/Layout';
import Home from "./pages/Home";
import { BookStoreThemeProvider } from './context/themeContext';
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
    {
        path: "/",
        element: <Home/>
    },
    {
        path: "/books",
        element: <div>도서 목록</div>
    }
])

function App() {

  return (
    <BookStoreThemeProvider>
      <Layout>
        <RouterProvider router={router}/>
      </Layout>
    
    </BookStoreThemeProvider>
  );
}

export default App;

API 통신

  • axios 라이브러리 사용

http 공통 모듈 Client

import axios, {AxiosRequestConfig} from "axios";
// AxiosRequestConfig = axios 요청에 대한 설정 옵션
const BASE_URL = "http://localhost:9999";
const DEFALUT_TIMEOUT = 30000;

export const createClient = (config?: AxiosRequestConfig) => {
    const axiosInstance = axios.create({
        baseURL: BASE_URL,
        timeout: DEFALUT_TIMEOUT,
        headers: {
            "content-type": "application/json", // 모든 요청에 대한 헤더 설정
        },
        withCredentials: true, // 요청에 자격증명 포함
        ...config // 함수 호출 시 전달된 추가 설정 병합
    })

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

export const httpClient = createClient();

category api

  • category api 응답 중 data를 return
import { Category } from "../models/category.model";
import { httpClient } from "./http"

export const fetchCategory = async () => {
    const response = await httpClient.get<Category[]>("/category");
    return response.data;
}

category hook

  • category fetch 커스텀 hook
  • 데이터를 분리 및 가공
import { useEffect, useState } from 'react';
import { Category } from '../models/category.model';
import { fetchCategory } from '../api/category.api';

export const useCategory = () => {
    const [category, setCategory] = useState<Category[]>([]);
  
    useEffect(()=>{
      fetchCategory().then((category)=>{
        if(!category) return;
        
        const categoryWithAll = [ // 데이터 가공
            {
                category_id: null,
                category_name: "전체",
            },
            ...category,
        ]
        setCategory(categoryWithAll);
      })
    },[]);
    return {category}
    }

import { useCategory } from '../../hooks/useCategory';

function Header() {
 const {category} = useCategory();
 // 필요한 컴포넌트에서 import하여 사용
  return (
    ...
  );
}

react-hook-form

  • 리액트 form 상태관리 라이브러리
npm install react-hook-form --save
import React, { useState } from 'react'
import styled from 'styled-components'
import Title from '../components/common/Title'
import InputText from '../components/common/InputText';
import Button from '../components/common/Button';
import { Link, useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { signup } from '../api/auth.api';
import { useAlert } from '../hooks/useAlert';

export interface SignupProps {
    email: string;
    password: string;
}

function Signup() {
    const navigate = useNavigate();
    const showAlert = useAlert()
    // const [email, setEmail] = useState("");
    // const [password, setPassword] = useState("");

    // const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    //     event.preventDefault();
    //     console.log(email, password)
    // }

    const {
        register, // input 값 register
        handleSubmit,
        formState: {errors}
    } = useForm<SignupProps>(); // 필요한 함수 register..을 사용

    const onSubmit = (data: SignupProps) => {
        signup(data).then((res)=>{
            showAlert("회원가입이 완료되었습니다.")
            navigate("/login")
        })
    }
  return (
    <>
    <Title size='large'>회원가입</Title>
    <SignupStyle>
      <form onSubmit={handleSubmit(onSubmit)}>
        <fieldset>
            <InputText placeholder='이메일'
            inputType="email"
            {...register("email", {required: true})}/> // required : 필수여부

            {errors.email && 
            <p className='error-text'>이메일을 입력해주세요</p>}
        </fieldset>
        <fieldset>
            <InputText placeholder='비밀번호'
            inputType="password"
            {...register("password", {required: true})}/>
            {errors.password && 
            <p className='error-text'>비밀번호를 입력해주세요</p>}
        </fieldset>
        <fieldset>
            <Button type="submit"
            size='medium' scheme='primary'>
                회원가입
            </Button>
        </fieldset>
        <div className="info">
            <Link to="/reset">비밀번호 초기화</Link>
        </div>
      </form>
    </SignupStyle>
    </>
  )
}
...
export default Signup

alert hook

  • window.alert() 와 같은 기능을 하지만 후에 alert 리팩토링을 위해 작성
import { useCallback } from "react";

export const useAlert = () => {
    const showAlert = useCallback((message: string) => {
    window.alert(message)
}, [])
    return showAlert;
}

컴포넌트

  • 비밀번호 초기화 (API 연동)
  • 로그인 전역상태 관리
  • 도서 목록 페이지

로그인

zustand 사용하여 로그인 상태 관리

npm i zustand --save

import { create } from "zustand";

interface StoreState { // 상태 정보와 액션 함수 함께 작성
    isloggedIn: boolean;
    storeLogin: (token: string) => void;
    storeLogout: () => void;
}

export const getToken = () => {
    const token = localStorage.getItem("token");
    return token;
}

const setToken = (token: string) => {
    localStorage.setItem("token", token);
}

export const removeToken = () => {
    localStorage.removeItem("token");
}

// store
export const useAuthStore = create<StoreState>((set) => ({ 
    // token 여부에 따라 로그인 상태를 관리함으로 새로고침해도 로그인 상태 유지
    isloggedIn: getToken() ? true : false,
    storeLogin: (token: string) => {
        set({isloggedIn: true});
        setToken(token);
    },
    storeLogout: () => {
        set({isloggedIn: false});
        removeToken();
    }
}))
  • 필요한 컴포넌트에서 import하여 사용
const { isloggedIn, storeLogin, storeLogout } = useAuthStore();

header에 token 넣어주기

import axios, {AxiosRequestConfig} from "axios";
import { getToken, removeToken } from "../store/authStore"

const BASE_URL = "http://localhost:9999";
const DEFALUT_TIMEOUT = 30000;

export const createClient = (config?: AxiosRequestConfig) => {
    const axiosInstance = axios.create({
        baseURL: BASE_URL,
        timeout: DEFALUT_TIMEOUT,
        headers: {
            "content-type": "application/json",
           **** Authorization: getToken() ? getToken() : "", ****
            // null로 return할 수도 있으므로 "" return
        },
        withCredentials: true,
        ...config
    })

    axiosInstance.interceptors.response.use(
        (response) => {
        return response;
    },
        (error)=> {
            // 로그인 만료 처리
            if(error.response.status === 401){
                removeToken();
                window.location.href = "/login"
                return;
            }
        return Promise.reject(error);
    })
    return axiosInstance;
}

export const httpClient = createClient();

도서 목록 화면 요구사항

  • 도서 목록을 fetch하여 화면에 렌더
  • pagination 구현
  • 검색 결과 없을 때 화면
  • 카테고리 및 신간 필터 기능
  • 목록 view는 그리드 형태, 목록 형태로 변경 가능

Bookfilter

  • 카테고리
  • 신간 여부

Query String

  • 상태 공유
  • 재사용성
  • 검색엔진 최적화

0개의 댓글