2024.03.15 TIL - dynamic route에 같은 메서드를 두번 써야할땐?!, custom hook에서의 상태관리(feat.zustand)

Innes·2024년 3월 15일
0

TIL(Today I Learned)

목록 보기
90/147
post-thumbnail

dynamic route의 활용!

📝 [id] route에 이미 todo의 isDone 상태를 toggle하는 PATCH api가 존재하는 상황에서, todo의 내용을 수정(update)하는 로직의 PATCH도 만들고 싶었는데, 같은 route파일에서는 PATCH를 한번만 선언할 수 있다?!
이럴땐 어떻게 해야할까?

  • 기존 코드
// ✅ Csr.tsx - todoList(next.js)

  // ...생략
  // ⭐️ todo의 isDone상태를 toggle하는 mutation 함수
  const { mutate: toggleTodo } = useMutation({
    mutationFn: async ({ id, isDone }: { id: string; isDone: boolean }) => {
      await fetch(`http://localhost:3000/api/todos/${id}`, {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
        },
        // ⭐️ body로 isDone을 직접 보내줌
        body: JSON.stringify({ isDone }),
      });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ["todos"],
      });
    },
  });

  // ⭐️ todo에 새로운 내용으로 update하는 함수
  const { mutate: updateTodo } = useMutation({
    mutationFn: async ({
      id,
      nextTitle,
      nextContents,
    }: {
      id: string;
      nextTitle: string;
      nextContents: string;
    }) => {
      const response = await fetch(`http://localhost:3000/api/todos/${id}`, {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
        },
        // ⭐️ body에 새롭게 입력받은 내용을 전달함
        body: JSON.stringify({ nextTitle, nextContents }),
      });
    },
  });
  // ...생략
// ✅ route.ts - app / api / todos / [id]

// ⭐️ db에 접근하여 isDone상태를 toggle하는 PATCH api
export async function PATCH(
    request: Request,
    { params }: { params: { id: string } }
  ) {
    const { isDone } = await request.json();
    const id = params.id;
  
    const response = await fetch(`http://localhost:4005/todos/${id}`, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
      },
      // ⭐️ isDone상태를 반대로 바꿔주는 내용을 body로 전달
      body: JSON.stringify({ isDone: !isDone }),
    });
  
    return Response.json({
      id,
      isDone,
    });
  }

// ❌ update하는 PATCH로직을 아래에 새로 만들려고 시도하니 PATCH를 재선언 할 수 없다는 오류발생!
  • 해결방법 1
    • [id] 폴더 안에 toggle폴더와 route.ts를 하나 더 만들기
    • toggle하는 PATCH api를 옮기기
    • [id] 폴더의 route.ts에서는 update하는 PATCH 작성하기
// ✅ Csr.tsx - todoList(next.js)

// toggle - url 수정
const { mutate: toggleTodo } = useMutation({
    mutationFn: async ({ id, isDone }: { id: string; isDone: boolean }) => {
      // ⭐️ [id] 폴더 안에 toggle폴더 하나 더 만들어서 url 수정함
      await fetch(`http://localhost:3000/api/todos/${id}/toggle`, {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ isDone }),
      });
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ["todos"],
      });
    },
  });

  const { mutate: updateTodo } = useMutation({
    mutationFn: async ({
      id,
      nextTitle,
      nextContents,
    }: {
      id: string;
      nextTitle: string;
      nextContents: string;
    }) => {
      const response = await fetch(`http://localhost:3000/api/todos/${id}`, {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ nextTitle, nextContents }),
      });
    },
    // 성공 후 id state도 빈값으로 돌려야 input에서 p태그로 바뀜
    onSuccess: () => {
      setSelectedId("");
      queryClient.invalidateQueries({
        queryKey: ["todos"],
      });
    },
  });
// ✅ route.ts - app / api / todos / [id]

// ⭐️ [id]의 route에서는 updateTodo PATCH api
export async function PATCH(
  request: Request,
  { params }: { params: { id: string } }
) {
  const id = params.id;
  const { nextTitle, nextContents } = await request.json();

  const response = await fetch(`http://localhost:4005/todos/${id}`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
    },
    // body에 전달하는 값으로 db가 수정된다.
    body: JSON.stringify({ title: nextTitle, contents: nextContents }),
  });

  return Response.json({
    id,
  });
}
// ✅ route.ts - app / api / todos / [id] / toggle

// 기존과 동일, 위치만 [id] / toggle 폴더 안의 route로 옮김
export async function PATCH(
  request: Request,
  { params }: { params: { id: string } }
) {
  const { isDone } = await request.json();
  const id = params.id;

  const response = await fetch(`http://localhost:4005/todos/${id}`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ isDone: !isDone }),
  });

  return Response.json({
    id,
    isDone,
  });
}
  • 해결방법 2
    • PATCH 안에서 toggle, update를 동시에 해결하기
    • body로 isDone, nextTitle, nextContents 보낼 때 state를 달리 해서 보내기
    • PATCH 안에서 조건문으로 구현
      (body로 들어온 state가 isDone의 state인 경우 toggle하는 로직, nextTitle과 nextContents의 state인 경우는 내용을 수정하는 로직)

해결방법 2 보다는, 간단하고 효율적인 1번 방법을 사용하자.
현업에서는 1번 방법 사용 시 백엔드와 조율이 필요하다는 점만 기억하자!


custom hook에서의 상태관리

  • 문제상황 : custom hook으로 mutation함수들을 분리하고, 거기에 useState로 상태관리까지 넣어놨는데, 분명 똑같은 코드를 컴포넌트 분리한 것 뿐인데 분리하기 전엔 잘 되던 코드가 분리하고 나니까 오류는 안났지만 제대로 동작하지 않았다.

    • '수정'버튼 누르면 title, contents 태그가 input태그로 변하면서 값을 입력할 수 있게 만들고 싶었다.
    • 컴포넌트 분리 전엔 잘 동작했는데, custom hook으로 분리했을때도 잘 됐는데, 버튼들을 컴포넌트로 분리하고나니까 갑자기 수정버튼을 아무리 눌러도 input으로 바뀌질 않았다.
  • 원인 : custom hook에서 관리하는 useState 상태관리는 만약 자식 컴포넌트까지 그 상태 변화가 전달되어야할때는 부적합한 사용이었다.

    • custom hook에서 상태가 변경된다 하더라도, 각 컴포넌트에서 각자 useState 쓴 것처럼 동작할뿐, 전역상태처럼 그렇게 모두에게 상태변화가 공유되지 않는 것이었다.
  • 해결 : 전역상태관리를 통해 상태관리를 해야한다!

👍🏻 그렇다면 이 참에 Zustand를 써보자!
공식문서 : https://docs.pmnd.rs/zustand/getting-started/introduction

zustand 사용법

  • Redux에서는 provider로 store를 내려줘야했는데, zustand에서는 그냥 custom hook 가져오듯이 useTodoStore(예시) 에서 state와 리듀서 함수 가져오면 됨

  • 가져올때는 useSelector 쓰는 것처럼 안에 콜백함수로 인자는 state, 결과값에는 state.초기값 혹은 state.함수 이렇게 가져와서 쓰면 된다! Redux 혹은 RTX보다 아주 간단하다

  • 예시 코드

// ✅ useUpdateStore.ts

import { create } from "zustand";

export interface StateType {
  selectedId: string;
  nextTitle: string;
  nextContents: string;
}

// store 생성하기(use로 시작해야함, 인자로 set이 들어옴)
export const useUpdateStore = create((set) => ({
  // 초기값(이름은 임의로 지정가능)
  updateState: {
    selectedId: "",
    nextTitle: "",
    nextContents: "",
  },
  // 액션 생성자 함수
  updateSelectedId: (id: string) =>
    set((state: StateType) => ({ ...state, selectedId: id })),
  updateTitle: (nextTitle: string) =>
    set((state: StateType) => ({ ...state, nextTitle: nextTitle })),
  updateContents: (nextContents: string) =>
    set((state: StateType) => ({
      ...state,
      nextContents: nextContents,
    })),
}));
// ✅ 컴포넌트에서 사용하는 예시

// store에서 바로 state와 액션 생성자 함수를 가져올 수 있다.
const { selectedId, nextTitle, nextContents } = useUpdateStore(
    (state: any): StateType => state.updateState
  );
  const updateSelectedId = useUpdateStore(
    (state: any) => state.updateSelectedId
  );
  const updateTitle = useUpdateStore((state: any) => state.updateTitle);
  const updateContents = useUpdateStore((state: any) => state.updateContents);
profile
무서운 속도로 흡수하는 스폰지 개발자 🧽

0개의 댓글