📍 이전에 node.js로 진행했던 book-store 프로젝트와 연동하기 위한 React 프로젝트 (1)
☑️ 사용 기술 스택
✅React + ts
✅zustand
✅styled components (sanitize)
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck"
},
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"
};
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
})
npm i react-router-dom @types/react-router-dom --save
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;
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();
import { Category } from "../models/category.model";
import { httpClient } from "./http"
export const fetchCategory = async () => {
const response = await httpClient.get<Category[]>("/category");
return response.data;
}
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 (
...
);
}
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
window.alert()
와 같은 기능을 하지만 후에 alert 리팩토링을 위해 작성import { useCallback } from "react";
export const useAlert = () => {
const showAlert = useCallback((message: string) => {
window.alert(message)
}, [])
return showAlert;
}
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();
}
}))
const { isloggedIn, storeLogin, storeLogout } = useAuthStore();
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();