본문 바로가기

개발 공부/React

Next.js 에서 모달 띄우기와 서버작업을 통한 유저 정보 받기 및 등록 (3)

코드 출처 : https://github.com/Cluster-Taek/next14-boilerplate

 

GitHub - Cluster-Taek/next14-boilerplate

Contribute to Cluster-Taek/next14-boilerplate development by creating an account on GitHub.

github.com

 

users/page.tsx

'use client';

const Page = () => {
  const pageStyle = PageSva();
  const { openModal } = useModals();

  const handleClickCreateButton = () => {
    openModal({
      id: MODAL.USER_CREATE,
      component: <UserCreateFormModal />,
    });
  };

  return (
    <Box className={pageStyle.wrapper}>
      <Button onClick={handleClickCreateButton}>Create</Button>
    </Box>
  );
};

export default Page;

 

user-create-form-modal.tsx

import Button from '@/components/common/button';
import { MODAL } from '@/constants/modal-key-constants';
import useModals from '@/hooks/use-modals';
import { IUserCreateFormValue, useCreateUserMutation } from '@/lib/user';
import { sva } from '@/styled-system/css';
import { Controller, useForm } from 'react-hook-form';

const UserCreateFormModal = () => {
  const userCreateFormStyle = UserCreateFormModalSva();
  const { closeModal } = useModals();

  const {
    handleSubmit: formHandleSubmit,
    formState,
    control,
    setError,
  } = useForm<IUserCreateFormValue>({
    defaultValues: {
      name: '',
    },
  });

  const { mutate: createUser } = useCreateUserMutation();

  const handleSubmit = formHandleSubmit(async (data) => {
    createUser(data, {
      onSuccess: () => {
        closeModal(MODAL.USER_CREATE);
      },
      onError: (error) => {
        setError('name', {
          type: 'manual',
          message: error.message,
        });
      },
    });
  });

  return (
    <form onSubmit={handleSubmit} className={userCreateFormStyle.wrapper}>
      <Controller
        control={control}
        name="name"
        rules={{ required: 'Name is required' }}
        render={({ field }) => (
          <input type="text" {...field} className={userCreateFormStyle.input} placeholder="name" />
        )}
      />
      {formState.errors.name && <div className={userCreateFormStyle.error}>{formState.errors.name.message}</div>}
      <Button type="submit">Submit</Button>
    </form>
  );
};

export default UserCreateFormModal;

const UserCreateFormModalSva = sva({
  slots: ['wrapper', 'input', 'button', 'error'],
  base: {
    wrapper: {
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'center',
      borderRadius: 'md',
      shadow: 'md',
      padding: '4',
      width: 'sm',
      margin: '0 auto',
      backgroundColor: 'white',
    },
    input: {
      width: 'full',
      padding: '2',
      marginTop: '2',
    },
    button: {
      width: 'full',
      padding: '2',
      marginY: '2',
      borderRadius: 'md',
      backgroundColor: 'primary.01',
      color: 'white',
      '&:hover': {
        backgroundColor: 'primary.02',
      },
    },
    error: {
      color: 'red',
      marginTop: '2',
    },
  },
});

 

UserCreateFormModal

  • 사용자 생성 폼 모달 컴포넌트입니다.
  • 폼 제출과 유효성 검사 로직을 포함합니다.
  • react-hook-form과 react-query를 사용하여 상태 관리 및 서버 요청을 처리합니다.

사용된 주요 라이브러리와 기능

  • react-hook-form: 폼 상태를 관리하고 유효성 검사를 수행합니다.
    • useForm 훅으로 폼 상태와 유효성 검사를 정의합니다.
    • Controller 컴포넌트를 사용하여 react-hook-form의 제어 하에 커스텀 컴포넌트를 연결합니다.
  • react-query: 데이터 요청 및 캐싱을 관리합니다.
    • useCreateUserMutation 훅으로 사용자 생성 API 호출을 처리합니다.
  • @styled-system/css 및 sva: 스타일링을 담당합니다.
    • sva를 사용하여 스타일링을 컴포넌트화하고 구조적으로 관리합니다.
  • useModals: 모달 관리 훅입니다.
    • 모달을 닫는 동작(closeModal)을 수행합니다

코드 동작

  1. 폼 상태 초기화
    • useForm 훅을 사용하여 기본 상태(defaultValues)를 { name: '' }으로 설정합니다.
  2. 폼 유효성 검사
    • Controller 컴포넌트로 폼 필드를 제어하며, rules를 통해 이름 입력 필수 조건을 설정합니다.
    • 이름이 비어있을 경우 'Name is required' 에러 메시지가 표시됩니다.
  3. 사용자 생성 요청 처리
    • useCreateUserMutation의 mutate를 호출하여 서버에 데이터를 전송합니다.
    • 요청 성공 시 모달을 닫습니다(closeModal).
    • 요청 실패 시 setError로 폼 에러 메시지를 설정합니다.
  4. UI 스타일링
    • UserCreateFormModalSva를 사용하여 스타일링을 설정합니다.
    • 각 UI 요소(wrapper, input, button, error)의 스타일을 정의합니다.
    • CSS-in-JS 방식을 통해 스타일과 컴포넌트를 통합적으로 관리합니다.

주요 컴포넌트 설명

<form>

  • 폼 제출 이벤트(onSubmit)를 처리합니다.
  • handleSubmit 함수는 폼 데이터와 API 호출 로직을 실행합니다.

<Controller>

  • 이름 입력 필드를 관리합니다.
  • 유효성 검사 실패 시 오류 메시지를 렌더링합니다.

Button

  • type="submit" 속성을 사용해 폼 제출을 트리거합니다.

UserCreateFormModalSva

  • sva 함수로 정의된 스타일 객체입니다.
  • 각 UI 요소에 재사용 가능한 스타일을 적용합니다.

 

useForm

const {
  handleSubmit: formHandleSubmit,
  formState,
  control,
  setError,
} = useForm<IUserCreateFormValue>({
  defaultValues: {
    name: '',
  },
});

 

  • control: 폼 필드를 제어합니다.
  • formState: 폼의 상태를 나타내며, 에러 정보를 포함합니다.
  • setError: 폼 필드에 수동으로 에러를 설정합니다.
  • handleSubmit: 폼 제출 핸들러를 감싸서 유효성 검사를 수행합니다.

 

const { mutate: createUser } = useCreateUserMutation();

  const handleSubmit = formHandleSubmit(async (data) => {
    createUser(data, {
      onSuccess: () => {
        closeModal(MODAL.USER_CREATE);
      },
      onError: (error) => {
        setError('name', {
          type: 'manual',
          message: error.message,
        });
      },
    });
  });

 

  • useCreateUserMutation
    • React Query를 사용해 정의된 커스텀 훅입니다.
    • 사용자 생성 API를 호출하는 로직을 캡슐화합니다.
    • 반환된 객체에서 mutate 메서드를 createUser라는 이름으로 구조 분해 할당했습니다.
    • mutate는 API 요청을 실행하는 함수입니다.
  • formHandleSubmit
    • react-hook-form에서 제공하는 함수입니다.
    • 폼 데이터를 검증한 뒤, 제출 핸들러를 실행합니다.
    • 이 코드에서는 handleSubmit이 폼 제출 시 호출됩니다.
  • data
    • 폼에서 제출된 데이터를 담고 있습니다.
    • 여기서는 사용자의 입력값 { name: '' } 형태로 전달됩니다.
  • createUser(data, { onSuccess, onError })
    • createUser는 사용자 생성 API 호출을 실행합니다.
    • 첫 번째 인수로 폼 데이터를 전달합니다.
    • 두 번째 인수로 성공실패 콜백을 포함한 옵션 객체를 전달합니다.

 

user.ts

import { fetchApi } from './base';
import { IPageable } from '@/types/pageable';
import { IUser } from '@/types/user';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

export interface IUsersParams {
  // paging params of json-server
  _page: number;
  _per_page: number;
}

export interface IUserCreateFormValue extends Record<string, unknown> {
  name: string;
}

export const useUsers = (params: IUsersParams) => {
  return useQuery<IPageable<IUser>>({
    queryKey: [`/api/users`, params],
  });
};

export const useCreateUserMutation = () => {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: async (data: IUserCreateFormValue) => await fetchApi.post(`/api/users`, data),
    onSuccess: async () => {
      await queryClient.invalidateQueries({
        queryKey: ['/api/users'],
        refetchType: 'all',
      });
    },
  });
};
  • 이 코드는 React Query를 활용하여 사용자 데이터를 조회하고, 새로운 사용자를 생성하는 두 가지 주요 기능을 제공하는 유틸리티 모듈입니다.

사용된 주요 라이브러리

  • fetchApi: API 호출을 담당하는 커스텀 함수입니다. 
  • @tanstack/react-query: 데이터의 캐싱, 상태 관리, 서버와의 비동기 작업을 효율적으로 관리하기 위한 라이브러리입니다.
    • useQuery: 데이터를 조회하고 캐싱하는 데 사용됩니다.
    • useMutation: 데이터를 생성하거나 수정, 삭제하는 등의 작업에 사용됩니다.
    • useQueryClient: React Query의 QueryClient 객체를 반환하며, 캐시를 직접 조작하거나 갱신하는 데 사용됩니다.
export interface IUsersParams {
  _page: number;    // 현재 페이지
  _per_page: number; // 페이지 당 아이템 개수
}

export interface IUserCreateFormValue extends Record<string, unknown> {
  name: string; // 사용자 이름
}

 

  • IUsersParams: 사용자 목록을 요청할 때 사용하는 쿼리 매개변수 형식을 정의합니다.
  • IUserCreateFormValue: 새 사용자를 생성할 때 전송하는 데이터 형식을 정의합니다.
    • Record<string, unknown>를 확장하여 추가 필드를 허용합니다.

사용자 조회 (useUsers)

  1. useUsers 훅 호출 시, React Query가 /api/users와 params를 키로 캐시를 확인합니다.
  2. 캐시가 유효하지 않으면 서버에서 데이터를 가져옵니다.
  3. 데이터를 성공적으로 가져오면 IPageable<IUser> 형태로 반환합니다.

useUsers 훅

export const useUsers = (params: IUsersParams) => {
  return useQuery<IPageable<IUser>>({
    queryKey: [`/api/users`, params],
  });
};

 

  • 동작:
    • 사용자 목록 데이터를 가져옵니다.
    • React Query의 useQuery를 사용하여 서버 데이터를 캐싱하고, 쿼리 키에 따라 데이터를 관리합니다.
  • 매개변수:
    • params: 서버 요청 시 사용할 페이지 매개변수 (_page, _per_page).
  • 결과:
    • React Query의 useQuery에서 반환하는 값(데이터, 상태, 에러 등).
  • 제공하는 기능:
    • 캐싱: 동일한 쿼리 키로 다시 요청할 경우 서버로 데이터를 다시 가져오지 않고 캐시된 데이터를 반환.
    • 자동 리페치: 쿼리가 유효하지 않을 때 자동으로 데이터를 다시 가져옵니다.

사용자 생성 (useCreateUserMutation)

  1. useCreateUserMutation의 mutate를 호출하여 새로운 사용자 데이터를 서버에 POST 요청합니다.
  2. 요청 성공 시 onSuccess가 실행되어 /api/users와 관련된 캐시 데이터를 무효화합니다.
  3. 무효화된 쿼리는 자동으로 서버에서 데이터를 다시 가져와 UI를 업데이트합니다.

useCreateUserMutation 훅

export const useCreateUserMutation = () => {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: async (data: IUserCreateFormValue) => await fetchApi.post(`/api/users`, data),
    onSuccess: async () => {
      await queryClient.invalidateQueries({
        queryKey: ['/api/users'],
        refetchType: 'all',
      });
    },
  });
};

queryClient

  • useQueryClient 훅을 사용해 React Query의 QueryClient 인스턴스를 가져옵니다.
  • 쿼리의 캐시를 수동으로 갱신하거나 무효화하는 데 사용됩니다.

mutationFn

  • 사용자가 전송한 데이터를 기반으로 서버에 POST 요청을 보냅니다.
  • fetchApi.post를 통해 /api/users 엔드포인트에 데이터를 전송합니다.
  • IUserCreateFormValue 형식의 데이터를 받습니다.

onSuccess

  • mutationFn이 성공적으로 완료되었을 때 실행됩니다.
  • queryClient.invalidateQueries:
    • /api/users 키와 관련된 모든 캐시 데이터를 무효화하고, 서버에서 데이터를 다시 가져옵니다.
    • refetchType: 'all'은 모든 관련 쿼리를 다시 가져오도록 설정합니다.