본문 바로가기
카테캠/2단계

[7/7] hooks와 validate로 나누기

by 쪼꼬에몽 2025. 7. 7.

이제부터라도 카테캠에서 진행한 것들을 적어보려고 한다.

현재 만들고 있는 것은 주문하기 페이지이다.

주문하기 버튼을 눌렀을 때 빈 곳이 있으면 경고 문구를 띄우는 조건을 만들고 있다. 

import { isValidHypenPhone, isValidPhone } from '@/utils/phoneValidation.ts';

export const validateOrderForm = ({
                              message, setMessage,
                              sender, setSender,
                              receiverName, setReceiverName,
                              receiverPhone, setReceiverPhone,
                            }: any): boolean => {
  let valid = true;

  if (!message.text) {
    setMessage(prev => ({ ...prev, check: true }));
    valid = false;
  }

  if (!sender.text) {
    setSender(prev => ({ ...prev, check: true }));
    valid = false;
  }

  if (!receiverName.text) {
    setReceiverName(prev => ({ ...prev, check: true }));
    valid = false;
  }

  if (!receiverPhone.text) {
    setReceiverPhone(prev => ({ ...prev, check: true }));
    valid = false;
  } else if (!isValidPhone(receiverPhone.text) && !isValidHypenPhone(receiverPhone.text)) {
    setReceiverPhone(prev => ({ ...prev, checkPhoneForm: true }));
    valid = false;
  }

  return valid;
};

빈 칸이 있는 부분은 상태의 check를 true로 변경하고 valid는 false로 변경한다.

휴대폰은 휴대폰 형식과 옳지 않으면 checkPhoneForm을 ture로 변경한다.

이렇게 제출을 하였는데 피드백이 왔다.

validator 함수인데 SRP에 어긋난다는 피드백이다. validator는 valid 여부만 반환해야 하는데 현재는 상태 변경까지 하고 있다.

사실 아직 기능 구별에 대해서 익숙하지 않아 어떤 것이 어느 기능에 속해야할 지 판단이 잘 안된다.

그래서 ai한테 물어보고 하니 set 함수는 useHooks에서 해야 한다는 가르침을 받았다.

export default function useOrderHandler({
  id, count, message, setMessage,
  sender, setSender, receiverName, setReceiverName,
  receiverPhone, setReceiverPhone
}) {
  const navigate = useNavigate();
  // 클릭한 아이템 정보 가져오기
  const item = JSON.parse(localStorage.getItem('expandedList'))[id - 1];
  const itemName = item.name;
  const itemPrice = item.price.sellingPrice;
  const [price, setPrice] = useState(itemPrice * count);

  // 카운트 클릭 시마다 가격 업데이트
  useEffect(() => {
    setPrice(itemPrice * count);
  }, [count]);

  // 입력란이 비어있는지 확인
  const handleOrder = () => {
    const isValid = validateOrderForm({
      message, setMessage
      sender, setSender
      receiverName, setReceiverName
      receiverPhone, setReceiverPhone
    });
    

    if (!isValid) return;

    toast(renderOrderSuccessToast(itemName, count, sender.text, message.text) as React.ReactNode, {
        type: 'success',
        autoClose: 3000,
        style: { width: '400px' },
      }
    );

    setTimeout(() => {
      navigate('/');
    }, 1000);
  };

  return { price, handleOrder };
}

이것이 validateOrderForm의 hooks 파일이다. 

이제 여기에 상태 변경을 추가해야한다. validate에서는 상태 변경을 빼고

import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import { validateOrderForm } from '@/utils/validateOrderForm';
import { renderOrderSuccessToast } from '@/utils/toastContents';

export default function useOrderHandler({
  id, count, message, setMessage,
  sender, setSender, receiverName, setReceiverName,
  receiverPhone, setReceiverPhone
}) {
  const navigate = useNavigate();
  // 클릭한 아이템 정보 가져오기
  const item = JSON.parse(localStorage.getItem('expandedList'))[id - 1];
  const itemName = item.name;
  const itemPrice = item.price.sellingPrice;
  const [price, setPrice] = useState(itemPrice * count);

  // 카운트 클릭 시마다 가격 업데이트
  useEffect(() => {
    setPrice(itemPrice * count);
  }, [count]);

  // 입력란이 비어있는지 확인
  const handleOrder = () => {
    const { isValid, errors } = validateOrderForm({
      message,
      sender,
      receiverName,
      receiverPhone,
    });

    // validation에 따른 판단
    if (errors.message) setMessage(prev => ({ ...prev, check: true }));
    if (errors.sender) setSender(prev => ({ ...prev, check: true }));
    if (errors.receiverName) setReceiverName(prev => ({ ...prev, check: true }));
    if (errors.receiverPhone) setReceiverPhone(prev => ({ ...prev, check: true }));
    if (errors.receiverPhoneFormat) setReceiverPhone(prev => ({ ...prev, checkPhoneForm: true }));

    if (!isValid) return;

    toast(renderOrderSuccessToast(itemName, count, sender.text, message.text) as React.ReactNode, {
        type: 'success',
        autoClose: 3000,
        style: { width: '400px' },
      }
    );

    setTimeout(() => {
      navigate('/');
    }, 1000);
  };

  return { price, handleOrder };
}

validate에 있는 state를 바꾸는 state문을 가져온다. 

해당문이 error인지 확인하는 errors도 설정해준다. 

import type { PhoneFiledState, TextFieldState } from '@/types/orderForm.ts';
import { isValidHypenPhone, isValidPhone } from '@/utils/phoneValidation.ts';

interface ValidateInput {
  message: TextFieldState;
  sender: TextFieldState;
  receiverName: TextFieldState;
  receiverPhone: PhoneFiledState;
}

interface ValidationResult {
  isValid: boolean;
  errors: {
    message: boolean;
    sender: boolean;
    receiverName: boolean;
    receiverPhone: boolean;
    receiverPhoneFormat: boolean;
  }
}

export const validateOrderForm =
  ({
     message, sender, receiverName, receiverPhone
  }: ValidateInput): ValidationResult => {
  const errors = {
    message: !message.text,
    sender: !sender.text,
    receiverName: !receiverName.text,
    receiverPhone: !receiverPhone.text,
    receiverPhoneFormat: false,
  };

  if (
    receiverPhone.text &&
    !isValidPhone(receiverPhone.text) &&
    !isValidHypenPhone(receiverPhone.text)
  ) {
    errors.receiverPhoneFormat = true;
  }

  // 조건이 모두 false여야 true가 나옴
  const isValid = Object.values(errors).every(err => !err);

  return { isValid, errors };
}

validate에서는 error의 true와 false만 판단하게 만든다.

errors 객체를 만들어 text가 비었을 때를 기준으로 한다.

폰 Form에는 폰 형식 확인도 넣어 휴대폰 형식이 아니면 true를 반환한다.

isValid는 모든 에러가 false일 때 true가 나오게 한다.

 

페이지 만드는 것은 익숙해졌는데 기능별 파일 분리는 아직 익숙하지 않아서 많이 참고해서 만들고 있다.

계속 공부하면서 기능별 분리도 익숙해져야겠다.