Carrot market 정복 노트 [7] - "Refactoring"

Jay·2022년 3월 15일
2

이 페이지에서는, Refactoring 에 관한 내용이다. react hook form 를 프로젝트에 적용, api 만들기, custom-hook 만들기, helper funtion 만들기 등 꼭 알아야 될것들이 담겨져 있다. 그러므로, 많은 연습 또한 필요할 것이다.


1. Input 컴포넌트로 register(react hook form)객체 보내는 방법.

기존 코드들을 확인해 보면 type선언을 할때에 [key: string]: any; 로 모든 props 를 받아 올수 있게 해둔 것이 있다. 이것을 통해 그냥 사용할수 있지만, 이 페이지에서는 새로 변수를 할당하여 구현 할것이다.
이를 통하여 이전 페이지에서 다뤘던 React hook form을 이전에 만든 Input 컴포넌트로 보내고 Input 컴포넌트 내에서 register 같은 함수를 props으로 사용 할수 있게 된다.
추가 방법은 아래의 코드와 같다.

components/input.tsx

import type { UseFormRegisterReturn } from "react-hook-form";
// register 의 리턴 타입을 살펴보면 UseFormRegisterReturn 이라는것을 알수있다. 앞에 type 을 입력하는것을 잊지 말자.
interface InputProps {
  label: string;
  name: string;
  kind?: "text" | "phone" | "price";
  type: string;   // type 에 대한 type 선언이 없어 추가.
  register: UseFormRegisterReturn;      // 다음과 같이 register를 위한 타입 선언.
  required: boolean;
}
export default function Input({
  label,
  name,
  kind = "text",
  register, // register 추가.
  type,
  required,
}: InputProps) {
  return (


<input
       id={name}
       required={required}
       {...register} // 다음과 같이 register 함수를 prop 으로 사용하기 위해 불러온다.
       type={type}
/>
	);
    
  );
}

※ Point

Typescript 개발환경 이라면, register 의 리턴 type은 UseFormRegisterReturn 이며, input 태그 안에 {...register} 로 불러 올수 있고 이는 곧 prop 으로써 사용 가능하다.


2. register(react hook form)객체를 prop 으로 사용 하는 방법

pages/enter.tsx

import { useForm } from "react-hook-form";

interface EnterForm {
  email?: string;
  phone?: string;
} 

const Enter: NextPage = () => {
  const { register, handleSubmit, reset } = useForm<EnterForm>();

 const onEmailClick = () => {
    reset(); //email 버튼 클릭시 폼을 reset 시켜주기 위한 react-hook-form 함수
    setMethod("email");
  };
  
 const onValid = (data: EnterForm) => {
  console.log(data);
  };
  
   return (
   	<>
     	<form
          onSubmit={handleSubmit(onValid)}
          className="flex flex-col mt-8 space-y-4"
        >
         	{method === "email" ? (
         	<Input
                register={register("email", {
                  required: true,
                })} 
                // 이와 같은 방식으로 register을 prop으로 받아 사용할수 있게되는 것.
                name="email"
                label="Email address"
                type="email"
                required
            	/>
          	) : null}
          
        </form>
    </>
  
  	)
  }

※ Point

Input 컴포넌트 태그 내에서 register={register("email", {required: true,})} 이러한 형태로 register을 prop으로 활용 할수 있으며, register prop 안에서의 register의 사용방법은 동일하다.


3. API post 요청 방법

pages/enter.tsx

const [loading, setLoading] = useState(false); // api 요청 기간동안에 loading 상태를 보여주기 위한.

 const onValid = (data: EnterForm) => {
    setLoading(true);  // loading 상태 시작
    fetch("/api/users/enter", { // API 관련 폴더를 생성해 관리하자.
      method: "POST",
      body: JSON.stringify(data), // data를 json 의 string 형태로 body에 넣음. 
      headers: {
        "Content-Type": "application/json", 
      }, // headers를 설정하여 api 요청후 r, 
    }).then(() => {		// fetch 가 끝나면~
      setLoading(false); // loading 상태 끝
    });
  };
   return (
   <>
    <Button text={loading ? "Loading" : "Get one-time password"} />
   </>
   )
 }

pages/api/users/enter.tsx

import { NextApiRequest, NextApiResponse } from "next";
import client from "../../../libs/client";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") { // 만약 POST 요청이 아닌 다른것이 왔을때의 조건문
    res.status(401).end();     //Unauthorized
  }
  console.log(req.body.email);
  res.status(200).end();  	   // OK
}

※ Point

  • 먼저, 주의 해야될점은 req.body가 아닌 req.body.(이곳)의 데이터를 보려 할경우 데이터가 나오지 않을것이다. 그 이유는, req의 내용이 인코딩 기준으로 parse 되기 때문이다. 이것을 해결하려면, 우선 클라이언트에서 Json 형태의 데이터를 보낸다는 것을 알려주어야 하기에, Json request는 "Content-Type": "application/json"으로 설정해주게 되면, 이 문제점이 해결된다. 자세한 내용은 Headers/Content-Type 를 통해 확인할수 있다.

  • API 호출 fetch("url", {method: ,body: ,headers:{}})

HTTP 상태 코드 Doc -> HTTP 상태 코드 링크


4. Custom hook (for API)만들기

libs/client/useMutation.tsx

import { useState } from "react";

interface UseMutationState {
  loading: boolean;
  data?: object;
  error?: object;
}
type UseMutationResult = [(data: any) => void, UseMutationState];
// type 선언을 코드정리 차원에서 따로 해두었는데, useMutation 의 return type은 첫번째는 함수 이기에 void 그리고 두번째는 {loading, data, error} 이다.

export default function useMutation(url: string): UseMutationResult {
   const [state, setState] = useState<UseMutationState>({
    loading: false,  //초기값 false
    data: undefined, // 초기값 undefined
    error: undefined,
  });
  //  useState를 통합하여 쓰는 방법.
  
  function mutation(data: any) {}  // 어떠한 인자든지 받을수 있게 any type으로 지정해준다.
   setState((prev) => ({ ...prev, loading: true }));
    fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data), // 여기서 data 는 enter 페이지에서 유저가 입력한 email 또는 phone 정보가 들어가게 된다.
    })
      .then((response) => response.json().catch(() => {})) 
      // response.json() 을 하게되면 promise를 받게 되므로 catch를 적용 catch에 빈 화살표 함수는 json 이 없어도 error 가 보이지 않게 하기 위해.
      .then((data) => setState((prev) => ({ ...prev, data })))
      //  setState 중에 data 담기
      .catch((error) => setState((prev) => ({ ...prev, error })))
      // setState 중에 error 에 대한 catch
      .finally(() => setState((prev) => ({ ...prev, loading: false })));
      // 다 마치게 되면, 로딩의 state를 false 로 변경.
  }		
  return [mutation, { ...state }];
  // mutation 함수 그리고 {loading, data, error} 를 리턴 시켜준다.
}

※ Point - 1

  • 여기서, useMutation의 return type은 (함수, (객체))로 할것.
  • type UseMutationResult = [(data: any) => void, UseMutationState]; 이 처럼 custome hook 의 리턴 type을 만들어 주어야 한다.
  • mutation 함수의 매개변수(data)는 enter page에서 mutation 함수가 호출 되어질때, 인자 값을 받아 이것을 JSON.stringify(data) 형태로 body에 넣어 준다.
  • 만약 useState의 선언이 많아질 경우에는 아래와 같은 코드로 활용 가능하다.
const [state, setState] = useState<type>({ A : false,  B string, C undefined,}); // 초기값 A: false , B: string, C:undefined

setState((prev) => ({ ...prev, A: true }));
// setState 하는 방법.
  • 비동기 함수는 항상 promise를 리턴하기 때문에 response.json(), fetch()와 같은 비동기 함수에 catch를 적용 해주어야 된다. fetch() 의 두번째 인자는 초기값을 위해 사용 되며 메소드 요청시에 사용 된다.

pages/enter.tsx

import useMutation from "../libs/client/useMutation";

interface EnterForm {
  email?: string;
  phone?: string;
}

 const [enter, { loading, data, error }] = useMutation("/api/users/enter");
 // 첫번째는 enter 함수로 fetch를 실행하는 mutation 함수를 의미 하고. 두번째로는 loading,data,error가 담긴 객체. useMutation 에서는 custom-hook이 있는 API 주소 url 을 입력 해주어야 된다.

 const onValid = (validForm: EnterForm) => {
 if (loading) return; // API post 요청 시간이 걸리기 때문에 화면단에서는 loading 상태로 변경시켜줌
	enter(validForm); // 기존에 있었던 fetch 함수는 useMutation custom-hook에 넣어 두었고,enter은 fatch 실행을 의미 하며, 인자 자리에 위치한 validForm은 EnterForm 객체에 있는 것들을 의미 한다(email or phone).
};

<Button text={loading ? "Loading" : "Get one-time password"} />
 

※ Point - 2

  • custom-hook인 useMutation 을 사용 할때에는 정해둔 리턴 타입에 맞춰 사용해야 하며, useMutation 인자에는 custom-hook이 존재 하는 API 주소 url 을 적어 주어야한다.
  • enter 은 useMutation hook 안에 있는 mutation 이라는 함수를 call 한것인데 이때 인자 값에는 유저가 입력한 email 또는 phone 정보가 들어가게 된다.

5. Helper function(for API handling) 만들기

여기서 Helper function의 정확한 명칭은 Higher-order function 이다.
쉽게 말해, 한개의 function 의 return 을 다른 function 으로 함수를 겹으로 리턴 하므로써, 특정 function 을 customizing 할수 있는 것이다.

Helper function 사용 방법.

pages/api/users/enter.tsx

import { NextApiRequest, NextApiResponse } from "next";
import client from "../../../libs/server/client";
import withHandler from "../../../libs/server/withHandler";

async function handler(req: NextApiRequest, res: NextApiResponse) {
  console.log(req.body);
 return res.status(200).end();
}

export default withHandler("POST", handler);  
// withHandler function에서 POST method 와 handler 함수를 export.
또한 POST 로 요청 될때에만 동작되게 할것 이라는 의미.

libs/server/withHandler.ts

import { NextApiRequest, NextApiResponse } from "next";

export default function withHandler(
  method: "GET" | "POST" | "DELETE",
  fn: (req: NextApiRequest, res: NextApiResponse) => void 
  // fn 은 인자로 req 와 res 를 void 리턴하는 함수 이며, pages/api/users/enter.tsx 에 있는 handler 함수로 대체 되어진다.
) {
  return async function (req: NextApiRequest, res: NextApiResponse) {
    if (req.method !== method) {  
      return res.status(405).end(); 
      // req.method 가 없다면 http 405 코드(bad request)를 실행한다
    }
    try {
      await fn(req, res);  // fn 함수는 위쪽의 조건문이 통과 되면 실행되며, 에러가 있다면 catch 를 통해 에러를 return 해준다. 
    } catch (error) {
      console.log(error);
      return res.status(500).json({ error }); //server error(500) 코드를 실행
    }
  };
}

※ Point

  • 여기서의 key point는 withHandler 함수 안에 있는 fn 함수가 pages/api/users/enter.tsx 에 있는 handler 함수로 대체 되어지며, 이 fn 함수의 실제 역할인 res.status(200)을 return 정상 실행과 오류 등의 조건적 실행을 위해 withHandler 함수의 return에 조건들을 넣을수 있다.
    그리하여, pages/api/users/enter.tsx파일 내에 있는 res.status(200)을 return 하는 handler 함수를 조금은 더 많은 경우의 환경에서 사용할수 있게 되는 것이다.

  • 동작 부분을 살펴 보면, enter page UI 에서 사용자가 email 또는 phone 을 입력 제출 하게 되면, useMutation 함수 안에 있는 fetch 함수를 통해 api 호출과 POST 방식으로 변경되며 export default withHandler("POST", handler); 코드가 성립이되어, 백엔드에서 의 console.log(req.body); 출력 확인이 가능할수 있게 된다.

잠깐!
NextJS에서 어떤 함수든 return 값이 항상 있어야 한다. 하지만, withHaandler 와 같은 파일에 return 값이 없이 잘 작동되는 이유는 res.json()이나 res.status(200).end() 와 같은 것들은 자체에서 return이 void 가 포함되어 있기 때문이다. 하지만 IDE의 도움을 받고 싶다면, return을 붙이는것이 좋다.


이외 알아두어야 할것들

  • NextJS는 function에 export default 입력을 해주지 않으면 이 function 은 url 로 적용이 되지 않아 호출 되지 않는다.

  • typescript 에서 확장명 ts 와 tsx 의 차이점은 크게 jsx 문법이 있다면, tsx 확장명 으로 해주어야 jsx 문법 지원이 된다. 또한 파일의 구분 짓기 위해 사용 되어지기도 한다.

  • import 할때 많은 층의 구조를 가진 파일들의 경로가 조금은 복잡하게 보이기도 한다. ex)import client from "../../../libs/server/client"; 이처럼, 이런 경로를 간단히 해줄수 있게 하는 방법은 아래의 코드와 같이 tsconfig.json 파일에 추가해 주면 된다.

tsconfig.json

    "baseUrl": ".",  // baseUrl의 "."은 가장 바깥쪽을 의미한다.
    "paths": {
      "@libs/*": ["libs/*"],  // @libs/* 는libs 파일 안쪽에 어떠한 path 들을 포함하겠다는 의미 
      "@components/*": ["components/*"]
    }

이렇게 해주게되면 "@libs/server/client" 로 import가 가능해진다. 많은 폴더 구조를 가지게 된다면 유용하게 사용될 것이다.

profile
React js 개발자

0개의 댓글