๐ŸฅธReview/ํ”„๋กœ์ ํŠธ

ํ”„๋ฆฌ์˜จ๋ณด๋”ฉ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ + ๋ฆฌ์•กํŠธ์ฟผ๋ฆฌ ๊ณผ์ œ refactoring ์ผ์ง€

hellohailie 2023. 1. 13. 00:28

1. ๋งฅ๋ฝ ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด ๋ณ€์ˆ˜๋ช…์œผ๋กœ ํ†ต์ผ

์ฐธ๊ณ 

  • ํƒ€์ž…๋ณ„ ๋ณ€์ˆ˜๋ช…
    • boolean : is-, has-, can-, ...
    • function : get-, handle-, submit-, …
    • array : -s (ex. users.map(user ⇒ user.id)), …
  • ํ”ผํ•ด์•ผ ํ•  ๋ณ€์ˆ˜๋ช…
    • data, info, foo, user1, mdhms, …

์ด๋ฒคํŠธ ํ•จ์ˆ˜๋‚˜ api ํ•จ์ˆ˜๋ช…์„ ~ handler๋กœ ํ†ต์ผํ–ˆ์Šต๋‹ˆ๋‹ค.

 

onSubmit => submitHandler
changeButton => changeButtonHandler
handleEdit => editHandler
handleDelete => deleteHandler

 


2. typescript์—์„œ 'any' ์—†์• ๊ธฐ

Axios์˜ interceptors ๋ผ๋Š” ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ด์„œ ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋‚ด๊ธฐ ์ง์ „์— ๊ฐ€๋กœ์ฑ„์„œ ๊ณตํ†ต์ ์œผ๋กœ ํ•„์š”ํ•œ ํ—ค๋”์™€ ํ† ํฐ์„ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค. 

์ด๋•Œ config๋ฅผ any๋กœ ์„ค์ •ํ•˜์˜€๋‹ค. 

 

any๋ฅผ ์—†์• ๊ณ ์ž ๊ตฌ๊ธ€๋ง์„ ํ•œ ๊ฒฐ๊ณผ, header๊ฐ€ AxiosRequestConfig๋ฅผ ๊ฐ–๋Š”๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค. 

 

 

axios.create๋กœ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ , AxiosRequestConfig ํƒ€์ž…์„ ๊ฐ€์ง€๋Š” config ๊ฐ์ฒด๋ฅผ ์ธ์ž๋กœ ๋„˜๊ฒจ์ฃผ์—ˆ๋‹ค.

 

๐Ÿซ ์ˆ˜์ • ์ „ ์ฝ”๋“œ

import axios from "axios";

const axiosInstance = axios.create({
  baseURL: "http://localhost:8080",
  headers: {
    "Content-Type": "application/json; charset=utf-8",
  },
});

axiosInstance.interceptors.request.use(
  async (config: any) => {
    const token = localStorage.getItem("token");
    if (token) {
      config.headers["Authorization"] = `Bearer ${token}`; //์—ฌ๊ธฐ๋Š” accessToken
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default axiosInstance;

 

โšก๏ธ์ˆ˜์ • ํ›„ ์ฝ”๋“œ

import axios, { AxiosRequestConfig } from "axios";

const axiosInstance = axios.create({
  baseURL: "http://localhost:8080",
  headers: {
    "Content-Type": "application/json; charset=utf-8",
  },
});

axiosInstance.interceptors.request.use(
  async (config: AxiosRequestConfig) => {
    const token = localStorage.getItem("token");
    if (token) {
      config.headers = {};
      config.headers.Authorization = `Bearer ${token}`; //์—ฌ๊ธฐ๋Š” accessToken
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default axiosInstance;

 

3. typescript์—์„œ 'any' ์—†์• ๊ธฐ (2)

react-query๋ฅผ ์ด๋ฒˆ ์‹œ๊ฐ„์„ ํ†ตํ•ด์„œ ์ฒ˜์Œ ์‚ฌ์šฉํ•ด๋ณด์•˜๋Š”๋ฐ typescript์™€ ํ•จ๊ป˜ ์“ฐ๋‹ˆ ์˜ค๋ฅ˜๋ฅผ ๋” ๋งŽ์ด ๋งŒ๋‚ฌ๋‹ค. 

๋‹ค๋ฅธ ๋ถ„๋“ค์˜ ์ฝ”๋“œ๋ฅผ ์ฐธ๊ณ ํ•ด๋ณด๋‹ค๊ฐ€ ๋งŽ์€ ๋ถ„๋“ค์ด useQuery๋‚˜ useMutation์œผ๋กœ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ hook์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๋ณด์•˜๊ณ  hook์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๊น”๋”ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•ด์„œ ๋‚ด ์ฝ”๋“œ์—๋„ ์ ์šฉํ•ด๋ณด์•˜๋‹ค. 

 

useMutation์œผ๋กœ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ onSuccess์™€, onError์ธ ์ƒํƒœ๋กœ ๋‚˜๋ˆ„๊ณ  ์žˆ๋Š”๋ฐ ์ด๋•Œ ๋ฐ์ดํ„ฐ(loginData) ํƒ€์ž…์„

export interface AuthResponse { message: string; token: string; }


์ด๋ ‡๊ฒŒ ์„ค์ •ํ•ด์„œ ๋„ฃ์–ด์ฃผ๊ณ  ์žˆ๋Š”๋ฐ ์—๋Ÿฌ๊ฐ€ ๋‚˜๊ณ  ์žˆ์—ˆ๋‹ค. 

์ฐธ๊ณ ๋กœ onSuccess์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ˜์†”์— ์ฐ์–ด๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋‚˜์™€์„œ AuthResponse๋ฅผ ์œ„์™€ ๊ฐ™์ด ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
{message: '์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๊ทธ์ธ ํ–ˆ์Šต๋‹ˆ๋‹ค', token: 'eyJhbGciOiJIUzI1NiJ9.dGVzdEB0ZXN0LmNvbQ.ryq30DukSEV3o-wUkSBPVgBNQ8eBN0FO5Hog7vjsUiI'}

 

์—๋Ÿฌ ์ฝ”๋“œ

TS2769: No overload matches this call. Overload 1 of 4, '(mutationFn: MutationFunction<AuthResponse, UserInfo>, options?: Omit<UseMutationOptions<AuthResponse, any, UserInfo, unknown>, "mutationFn"> | undefined): UseMutationResult<...>', gave the following error.

 

๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

 

์œ„ ์ฝ”๋“œ์—์„œ useMutation์€ ๋‚ด๋ถ€ ์ธ์ž๋“ค์„ ๋ณด๊ณ  ํƒ€์ž…์„ ์ถ”๋ก ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ, ์ฒซ ๋ฒˆ์งธ ์ธ์ž์ธ mutationFn์˜ ํƒ€์ž…์„ ๋ณด๋ฉด AuthAPI.login(userInfo)์˜ ํƒ€์ž…์ด (data: UserInfo) => any๋กœ ์ถ”์ •๋˜๋Š” ๊ฑธ ๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

๊ทธ๋Ÿฌ๋ฉด onSuccess๋กœ ๋„˜์–ด๊ฐ€๋Š” ๋ฐ์ดํ„ฐ์˜ ํƒ€์ž…์€ any ํƒ€์ž…์ด ๋„˜์–ด๊ฐ€๋Š” ๊ฑธ ๊ธฐ๋Œ€ํ• ํ…๋ฐ AuthResponse ํƒ€์ž…์ด๋ผ๊ณ  ํ•˜๋‹ˆ๊นŒ ํƒ€์ž… ์—๋Ÿฌ๋ฅผ ๋‚ด๋Š”๊ฒƒ์ด์—ˆ๋‹ค!

 

์ฆ‰, AuthAPI.login์˜ ํƒ€์ž…์„ (data: UserInfo) => AuthResponse ํƒ€์ž…์œผ๋กœ ๋งŒ๋“ค๋ฉด ์—๋Ÿฌ๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค!

 

๋‚ด๋ถ€ ์ธ์ž์˜ ํƒ€์ž…์„ ํ™•์ธํ•ด์„œ ๋ฆฌํ„ดํ•˜๋Š” ํƒ€์ž…์„ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค!

 

๊ธฐ์กด ์ฝ”๋“œ

//AuthAPI

import axiosInstance from "./axiosInstance";
import { UserInfo } from "../types/model";

const AuthAPI = {
  signUp: (data: UserInfo) => {
    return axiosInstance.post("/users/create", data);
  },
  login: (data: UserInfo) => {
    return axiosInstance.post("/users/login", data);
  },
};

export default AuthAPI;

 

์ˆ˜์ • ์ฝ”๋“œ

//AuthAPI

import axiosInstance from "./axiosInstance";
import { UserInfo, AuthResponse } from "../types/model";

const AuthAPI = {
  signUp: (data: UserInfo) => {
    return axiosInstance.post("/users/create", data);
  },
  login: (data: UserInfo): Promise<AuthResponse> => {
    return axiosInstance.post("/users/login", data);
  },
};

export default AuthAPI;

 

4. ๋ฆฌ์•กํŠธ ์ฟผ๋ฆฌ ์ ์šฉ๊ณผ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌํ•˜๊ธฐ

axios์˜ interceptors๋ฅผ ์‚ฌ์šฉํ•ด์„œ baseURL๊ณผ ํ—ค๋”์— ๋“ค์–ด๊ฐˆ ํ† ํฐ์„ ์„ธํŒ…ํ–ˆ๋‹ค. 

 

๊ทธ๋ฆฌ๊ณ  authํŒŒํŠธ์™€ todoํŒŒํŠธ๋ฅผ ๋‚˜๋ˆ„์–ด์„œ interceptors๋ฅผ ์‚ฌ์šฉํ•˜๋Š” api๋ฅผ ๊ฐ๊ฐ ๋งŒ๋“ค์—ˆ๋‹ค. 

 

// AuthAPI
import axiosInstance from "./axiosInstance";
import { UserInfo, AuthResponse } from "../types/model";

const AuthAPI = {
  signUp: (data: UserInfo) => {
    return axiosInstance.post("/users/create", data);
  },
  login: (data: UserInfo): Promise<AuthResponse> => {
    return axiosInstance.post("/users/login", data);
  },
};

export default AuthAPI;

 

๊ทธ๋ฆฌ๊ณ  login, signup, get, post, put, delete hook์„ ๋งŒ๋“ค์–ด์„œ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ๋ฅผ ํ•ด๋ณด์•˜๋‹ค. 

์—ฌ๊ธฐ์„œ๋Š” get ๋ฉ”์„œ๋“œ์—๋Š” useQuery๋ฅผ, ๊ทธ ์™ธ์—๋Š” useMutation๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. 

 

๊ทธ๋ฆฌ๊ณ  ์ด hook์ด ํ•„์š”ํ•œ ๊ณณ์—์„œ ํ˜ธ์ถœ๋งŒ ํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์ง !๋‚˜์˜จ๋‹ค. 

ํŠนํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” useGetTodo hook์—์„œ๋Š” ๋กœ๋”ฉ ์ƒํƒœ ์œ ๋ฌด๋ฅผ ๋น ๋ฅด๊ฒŒ ํŒ๋‹จํ•ด์„œ ๋กœ๋”ฉ ์ƒํƒœ๋กœ ์†์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค!

 

refactoring ์ „

import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { TodoMD } from "../types/model";
import SingleTodo from "./SingleTodo";
import axiosInstance from "../api/axiosInstance";

interface Prop {
  submitHandler: (e: React.FormEvent<HTMLFormElement>) => void;
}

const TodoList = ({ submitHandler }: Prop) => {
  const [getData, setGetData] = useState<TodoMD[]>([]);

  useEffect(() => {
    axiosInstance.get("/todos").then((res) => setGetData(res.data));
  }, [submitHandler]);

  return (
    <Container>
      {getData?.map((todo: TodoMD) => (
        <SingleTodo key={todo.id} {...todo} />
      ))}
    </Container>
  );
};

export default TodoList;

๐Ÿ‘‰ useState๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ํ•ด์ฃผ๊ณ  ์žˆ๋‹ค. 

 

 

refactoring ํ›„

import React from "react";
import styled from "styled-components";
import { TodoMD } from "../types/model";
import SingleTodo from "./SingleTodo";
import useGetToDo from "../hooks/todos/useGetTodo";
import LoadingBar from "../components/LodingBar";

const TodoList = () => {
  const { data, isLoading } = useGetToDo();

  return (
    <Container>
      <LoadingBar isLoading={isLoading} />
      {data?.map((todo: TodoMD) => (
        <SingleTodo key={todo.id} {...todo} />
      ))}
    </Container>
  );
};

export default TodoList;

๐Ÿ‘‰ useGetToDo hook์„ ์‚ฌ์šฉํ•ด์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ , ๋กœ๋”ฉ์ƒํƒœ์ธ์ง€ ์•„๋‹Œ์ง€๋ฅผ ํŒ๋‹จํ•ด์„œ ๋กœ๋”ฉ์ค‘์ด๋ฉด ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ํ‘œ์‹œํ•œ๋‹ค. 

 

 

5. ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌํ•˜๊ธฐ (2)

auth ์š”๊ตฌ์‚ฌํ•ญ์ด ์•„๋ž˜์™€ ๊ฐ™์•˜๋‹ค. 

  • ์ด๋ฉ”์ผ ์กฐ๊ฑด : ์ตœ์†Œ @, . ํฌํ•จ
  •  ๋น„๋ฐ€๋ฒˆํ˜ธ ์กฐ๊ฑด : 8์ž ์ด์ƒ ์ž…๋ ฅ
  •  ์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ชจ๋‘ ์ž…๋ ฅ๋˜์–ด ์žˆ๊ณ , ์กฐ๊ฑด์„ ๋งŒ์กฑํ•ด์•ผ ์ œ์ถœ ๋ฒ„ํŠผ์ด ํ™œ์„ฑํ™” ๋˜๋„๋ก ํ•ด์ฃผ์„ธ์š”

refactoring ์ „

//Auth.tsx

const changeButtonHandler = () => {
    email.includes("@") && password.length >= 8
      ? setIsActive(true)
      : setIsActive(false);

๐Ÿ‘‰ ์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ input ์—์„œ onKeyUp ์ด๋ฒคํŠธ๋ฅผ ํ™œ์šฉํ•˜๋Š” ์‹์œผ๋กœ ํ–ˆ๋‹ค. (๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ X)

 

 

refactoring ํ›„

//Auth.tsx

import { emailCheck, passwordCheck } from "../../utils/validator";

const validatorHandler = () => {
    if (emailCheck(email) && passwordCheck(password)) {
      setIsActive(true);
    } else {
      setIsActive(false);
    }
  };
// validator.ts

function emailCheck(email: string): boolean {
  if (email.includes("@") && email.includes(".")) {
    return true;
  } else {
    alert("์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹™๋‹ˆ๋‹ค.");
    return false;
  }
}

function passwordCheck(password: string) {
  if (password.length >= 8) {
    return true;
  }
}

export { emailCheck, passwordCheck };

๐Ÿ‘‰ auth์˜ ์š”๊ตฌ์‚ฌํ•ญ์„ ํ™•์ธํ•˜๋Š” ๋ถ€๋ถ„์„ validator.ts ํ•จ์ˆ˜๋กœ ๋”ฐ๋กœ ๋ถ„๋ฆฌํ•˜์˜€๋‹ค.

 

๊ทธ๋ž˜์„œ ์ถ”๊ฐ€์ ์ธ ์š”๊ตฌ์‚ฌํ•ญ์ด ์ƒ๊ธฐ๊ฑฐ๋‚˜ ๋ฌธ์ œ์‚ฌํ•ญ์ด ๋ฐœ์ƒํ–ˆ์„๋•Œ ์ฝ์–ด์•ผ ํ•˜๋Š” ์ฝ”๋“œ์˜ ๋‹จ์œ„๊ฐ€ ์ค„์–ด๋“ค์–ด์„œ ์ฝ”๋“œ์— ๋Œ€ํ•œ ํŒŒ์•…์ด ๋นจ๋ผ์ง€๋ฏ€๋กœ ๋ฌธ์ œ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค!!