Skip to content

Conversation

@jinsunkimdev
Copy link

@jinsunkimdev jinsunkimdev commented Jun 25, 2025

비제어를 베이스로 해서 필요한 부분에만 제어를 사용하려고 했는데 헷갈리고 어렵네요...좀 더 생각을 해봐야 할 것 같습니다ㅠ
이전에 올렸던 PR의 주소입니다. #194

주요 변경사항

  • 기존에 useForm훅에서는 data를 useState()를 사용하고, addItem페이지에서는 input에 ref로 접근하는 부분을 useForm에서 getter,setter함수를 사용하여 모두 ref를 사용하여 관리를 하도록 통일하였습니다.
  • onChange를 통해 실시간 에러 체크를 할 때, 리렌더링이 빈번하게 발생되어서 이를 해결하기 위해서 최적화 하기 위해서 onChange시에는 에러 체크를 하지 않고 onBlur에만 입력값 에러 체크를 하도록 수정하였습니다.

스크린샷

React Hook Form을 사용해서 폼을 만들었을 때

Screen.Recording.2025-06-25.at.9.46.32.mov

현재 제가 만든 폼

Screen.Recording.2025-06-25.at.9.42.36.mov

React Hook Form을 addItem페이지에 적용한 코드입니다.

// src/components/AddItem.jsx
import React from "react";
import { useForm, Controller } from "react-hook-form";
import styles from "./Additem.module.css";
import ImageUpload from "./components/ImageUpload";
import TextInputField from "./components/TextInputField";
import TagInput from "./components/TagInput";
import { formatWithCommas } from "./utils/formatUtils";

export default function AddItem() {
  console.count(`rendering`)
  const {
    register,
    control,
    handleSubmit,
    setValue,
    formState: { errors, isValid },
  } = useForm({
    mode: "onBlur",
    reValidateMode: "onChange",
    defaultValues: {
      productName: "",
      productDescription: "",
      productPrice: "",
      productTag: [],
      uploadImage: null,
    },
  });

  const onSubmit = (data) => {
    // 콤마 제거 + 숫자로 변환
    const numericPrice = Number(data.productPrice.replace(/,/g, ""));
    console.log("제출 데이터:", { ...data, productPrice: numericPrice });
  };

  return (
    <form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
      <header className={styles.header}>
        <h1 className={styles.title}>상품 등록하기</h1>
        <button
          type="submit"
          className={styles.submitButton}
          disabled={!isValid}
        >
          등록
        </button>
      </header>

      <div className={styles.inputContainer}>
        {/* 이미지 업로드 */}
        <Controller
          name="uploadImage"
          control={control}
          rules={{ required: "이미지를 업로드해주세요" }}
          render={({ field }) => (
            <>
              <ImageUpload
                onImageChange={(file) => field.onChange(file)}
                previewUrl={
                  field.value ? URL.createObjectURL(field.value) : undefined
                }
              />
              <p className={styles.errorText}>
                {errors.uploadImage?.message || "\u00A0"}
              </p>
            </>
          )}
        />

        {/* 상품명 */}
        <TextInputField
          label="상품명"
          {...register("productName", { required: "상품명을 입력해주세요" })}
          placeholder="상품명을 입력해주세요"
          error={errors.productName?.message}
          wrapperClass={styles.inputWrapper}
          inputClass={styles.input}
          errorClass={styles.errorText}
        />

        {/* 상품 소개 */}
        <TextInputField
          as="textarea"
          label="상품 소개"
          {...register("productDescription", {
            required: "상품 소개를 입력해주세요",
          })}
          placeholder="상품 소개를 입력해주세요"
          error={errors.productDescription?.message}
          wrapperClass={styles.inputWrapper}
          textAreaClass={styles.textArea}
          errorClass={styles.errorText}
        />

        {/* 상품 가격 */}
        <Controller
          name="productPrice"
          control={control}
          rules={{
            required: "가격을 입력해주세요",
            pattern: {
              value: /^[0-9,]+$/,
              message: "숫자만 입력 가능합니다",
            },
          }}
          render={({ field }) => (
            <TextInputField
              label="상품 가격"
              name={field.name}
              value={field.value}
              onChange={(e) => field.onChange(e.target.value)}
              onBlur={(e) => {
                const formatted = formatWithCommas(e.target.value);
                setValue("productPrice", formatted, { shouldValidate: true });
                field.onBlur();
              }}
              placeholder="숫자만 입력해주세요"
              error={errors.productPrice?.message}
              wrapperClass={styles.inputWrapper}
              inputClass={styles.input}
              errorClass={styles.errorText}
            />
          )}
        />

        {/* 태그 입력 */}
        <Controller
          name="productTag"
          control={control}
          rules={{
            validate: (tags) =>
              tags.length > 0 || "태그를 최소 하나 이상 입력해주세요",
          }}
          render={({ field }) => (
            <>
              <TagInput
                name={field.name}
                tags={field.value}
                setTags={(newTags) => field.onChange(newTags)}
                onBlur={() => field.onBlur()}
                wrapperClass={styles.inputWrapper}
                inputClass={styles.input}
                tagContainerClass={styles.tagsContainer}
                tagClass={styles.tag}
                removeButtonClass={styles.removeTagButton}
              />
              <p className={styles.errorText}>
                {errors.productTag?.message || "\u00A0"}
              </p>
            </>
          )}
        />
      </div>
    </form>
  );
}

멘토에게

  • React Hook Form을 사용해서 폼을 만들었을 때는 onChange를 사용하더라도 입력되는 input만 리렌더링이 되는데 제가 만든 폼은 부모인 form컴포넌트 전체가 리렌더링이 됩니다. 어떻게 하면 React Hook Form처럼 구현할 수 있는 걸까요?

- 상품 이미지 업로드 (1장 제한, 미리보기)
- 상품명, 소개, 가격, 태그 입력 필드 구성
- 태그 입력 및 삭제 기능 구현
- 실시간 유효성 검사 및 에러 메시지 출력
- 등록 버튼 유효성 검사에 따라 비활성화 처리
- 이미지 등록 기능 (1장 제한, 미리보기 포함)
- 상품명, 소개, 가격 입력 필드 및 유효성 검사
- 태그 추가 및 제거 기능 (Enter로 입력)
- Form 유효성에 따라 등록 버튼 비활성화 처리
- 스타일 모듈화 및 폰트 색상 적용
@jinsunkimdev jinsunkimdev added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Jun 25, 2025
Copy link
Collaborator

@addiescode-sj addiescode-sj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

진선님,
궁금하셨던 부분에 대한 답변이 필요하셨던것같아 전체적으로 답변드리겠습니다 :)

우선 궁금해하셨던 왜 리액트 후크폼을 적용한것처럼 입력 시 개별 입력필드만 리렌더링되는게 안될까에 대해서는 구체적인 상태 관리 흐름과 방식을 추적해보실 필요가 있는데요!

제가 코드를 확인해보니 이런 문제점이 있네요!

  • 현재 useRef로 데이터를 잘 관리하고 있지만, 에러 상태는 useState로 관리되고있어 에러가 변경될 때마다 전체 컴포넌트가 리렌더링되고있습니다.(setErrors가 호출될 때마다 전체 컴포넌트가 리렌더링되고, 하나의 필드 에러만 변경되어도 모든 필드가 다시 렌더링되겠죠?)
  • 부모 컴포넌트의 상태가 변경되면, 당연히 전체가 리렌더링됩니다. (AddItem 컴포넌트의 errors state -> 각 인풋 필드가 독립적으로 리렌더링되지 않는 결과 발생)
  • 모든 에러가 하나의 상태 객체에 저장되어 있어, 하나의 필드 에러가 변경되면 전체가 리렌더링됩니다. (errors 객체 형태로 상태 관리)

이 부분을 아래 라인에서 짚어드려볼테니, 한번 보시고 고쳐보시겠어요? :)

Copy link
Collaborator

@addiescode-sj addiescode-sj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

즉, 개선안을 설명드리자면: 각 필드가 독립적인 상태를 유지할수있게끔 보장하고, 하나의 필드가 변경되어도 다른 필드는 리렌더링되지않는 구조가 될수있게끔 개선해주시면됩니다! :)


export const useForm = (options) => {
const dataRef = useRef({ ...options.initialValues });
const [errors, setErrors] = useState({});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRef로 폼 데이터는 관리하지만, 에러 상태는 useState로 관리

  • setErrors가 호출될 때마다 전체 컴포넌트가 리렌더링됨
  • 하나의 필드 에러가 변경되어도 모든 필드가 다시 렌더링됨

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모든 필드의 에러가 하나의 errors 객체에 저장됨

  • 하나의 필드 에러가 변경되면 전체 errors 객체가 새로 생성됨
  • React는 객체 참조가 변경되었으므로 모든 필드를 리렌더링함

onChange={handleChange("productName")}
onBlur={handleBlur("productName")}
placeholder="상품명을 입력해주세요"
error={errors.productName}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모든 입력 필드가 하나의 부모 컴포넌트 안에 직접 렌더링됨

  • 부모 컴포넌트의 errors 상태가 변경되면 모든 자식 컴포넌트가 리렌더링됨
  • 각 필드가 독립적으로 리렌더링되지 않음

@addiescode-sj addiescode-sj merged commit 30dfe47 into codeit-bootcamp-frontend:React-김진선 Jul 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants