본문 바로가기
코딩 정보/React

[React] useRef vs useState 중 무엇을 써야하지?

by 꽁이꽁설꽁돌 2024. 11. 12.
728x90
반응형

목차

     

    근본적인 질문에 대해 구체적인 것이 아니라 모호하게 알고 있어 이참에 정리하고자 한다.

     

    useState

    React에서 컴포넌트는 자신의 상태 또는 props가 바뀌면 리렌더링된다.

     

    useRef

    render 메소드에서 생성된 Dom 노드나 element에 접근하는 방법을 제공한다.

     

    바람직한 사용예)

    • 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때
    • 애니메이션을 직접적으로 실행시킬 때
    • 서드 파티 DOM 라이브러리를 React와 같이 사용할 때

     

    input에서의 useState와 useRef

    useState

    function Input() {
      const [value, setValue] = useState("")
      return <input value={value} onChange={(e) => setValue(e.target.value)} />
    }
    
    export default Input

     

    계속해서 리랜더링이 일어나는 모습

     

    useRef

    function Input() {
      const inputRef = useRef(null)
      return <input ref={inputRef} />
    }
    
    export default Input

     

    리랜더링이 일어나지 않는다.

     

    그렇다면 input에는 useState와 useInput중 무엇을 써야 할까?

    리액트 공식문서의 설명을 보기전 다음을 알아야 한다.

     

    Controlled VS Uncontrolled Component

    Controlled Component

    HTMLElement들의 상태를 엘리먼트를 가지고 있는 컴포넌트가 관리하면 Controlled Component라고 한다.

     

    대표적인 예

    • input
    • select
    • textarea

    Uncontrolled Component

    엘리먼트의 상태를 관리하지 않고 엘리먼트의 참조만 컴포넌트가 소유한다면, Uncontrolled Component이다.

     

     

    따라서 공식문서의 설명은 다음과 같다.

    대부분 경우에 폼을 구현하는데 제어 컴포넌트를 사용하는 것이 좋습니다.
    제어 컴포넌트에서 폼 데이터는 React 컴포넌트에서 다루어집니다.
    대안인 비제어 컴포넌트는 DOM 자체에서 폼 데이터가 다루어집니다.

     

    form의 input 유효성 검사 로직은 state의 변경에 따라 실행되어야 제어가 되기 때문에 state를 써주는 게 올바른

    방향이라고 볼 수 있다.

     

     

    비제어 컴포넌트의 예시

    다음은 내가 카카오 api를 통해 map을 조작했을 시의 코드이다.

    서드 파티 DOM 라이브러리를 React와 같이 사용할 때이기 때문에 map의 참조로 비제어 컴포넌트라고 볼 수 있다.

    const mapRef = useRef<kakao.maps.Map | null>(null);

     

     

    그렇다면 여러 제출 양식이 모여있는 form에서 모든 input에 state를 쓰는게 올바른 방향일까?

     

    useState의 상태를 묶어주면 바뀐부분만 랜더링하기 때문에 해결할 수 있다.

    다음과 같은 코드를 통해 해결할 수 있다.

    useState로 상태를 묶은 코드

    import React, { useState } from "react";
    
    function FormWithMultipleInputs() {
      const [inputValues, setInputValues] = useState({
        name: "",
        email: "",
        age: "",
        gender: "",
        country: "",
        subscribe: false,
        comments: ""
      });
    
      const handleChange = (e) => {
        const { name, value, type, checked } = e.target;
        const inputValue = type === "checkbox" ? checked : value;
    
        // 상태에 입력된 값 저장
        setInputValues((prev) => ({
          ...prev,
          [name]: inputValue,
        }));
      };
    
      const handleSubmit = (e) => {
        e.preventDefault();
        console.log("Submitted values:", inputValues);
        // 제출 후 입력 값 초기화
        setInputValues({
          name: "",
          email: "",
          age: "",
          gender: "",
          country: "",
          subscribe: false,
          comments: ""
        });
      };
    
      return (
        <form onSubmit={handleSubmit}>
          <label>
            Name:
            <input
              type="text"
              name="name"
              onChange={handleChange}
              value={inputValues.name}
            />
          </label>
          <label>
            Email:
            <input
              type="email"
              name="email"
              onChange={handleChange}
              value={inputValues.email}
            />
          </label>
          <label>
            Age:
            <input
              type="number"
              name="age"
              onChange={handleChange}
              value={inputValues.age}
            />
          </label>
          <label>
            Gender:
            <label>
              <input
                type="radio"
                name="gender"
                value="male"
                onChange={handleChange}
                checked={inputValues.gender === "male"}
              />
              Male
            </label>
            <label>
              <input
                type="radio"
                name="gender"
                value="female"
                onChange={handleChange}
                checked={inputValues.gender === "female"}
              />
              Female
            </label>
          </label>
          <label>
            Country:
            <select
              name="country"
              onChange={handleChange}
              value={inputValues.country}
            >
              <option value="">Select Country</option>
              <option value="korea">Korea</option>
              <option value="usa">USA</option>
              <option value="japan">Japan</option>
              <option value="china">China</option>
            </select>
          </label>
          <label>
            Subscribe:
            <input
              type="checkbox"
              name="subscribe"
              onChange={handleChange}
              checked={inputValues.subscribe}
            />
          </label>
          <label>
            Comments:
            <textarea
              name="comments"
              onChange={handleChange}
              value={inputValues.comments}
            />
          </label>
          <button type="submit">Submit</button>
        </form>
      );
    }
    
    export default FormWithMultipleInputs;

     

     

    위에 같이 하게 되면 유효성 검사를 할때 코드가 매우 복잡해진다.

    그래서 React Hook Form 라이브러리는 다음과 같은 방식으로 ref를 사용해 해결한다.

    유효성 검사를 위해 수정한 코드

    import React, { useRef, useState } from "react";
    
    function useForm() {
      const fieldsRef = useRef({});
      const [errors, setErrors] = useState({});
    
      // 유효성 검사 함수
      const validateField = (name, value, validationRules) => {
        let error = "";
        if (validationRules?.required && !value) {
          error = "This field is required";
        } else if (
          validationRules?.pattern &&
          !validationRules.pattern.test(value)
        ) {
          error = "Invalid format";
        }
        setErrors((prev) => ({
          ...prev,
          [name]: error,
        }));
      };
    
      // register 함수 - 필드를 등록하고 이벤트 핸들러 반환
      const register = (name, validationRules) => {
        return {
          name,
          ref: (el) => {
            if (el) fieldsRef.current[name] = { ref: el, validationRules };
          },
          onChange: (e) => {
            const value =
              e.target.type === "checkbox" ? e.target.checked : e.target.value;
            validateField(name, value, validationRules);
          },
        };
      };
    
      // 제출 처리 함수
      const handleSubmit = (onSubmit) => (e) => {
        e.preventDefault();
        const formData = {};
        let isValid = true;
        const newErrors = {};
    
        // 모든 필드의 유효성 검사
        Object.keys(fieldsRef.current).forEach((key) => {
          const { ref, validationRules } = fieldsRef.current[key];
          const value = ref.value;
          formData[key] = value;
    
          if (validationRules?.required && !value) {
            isValid = false;
            newErrors[key] = "This field is required";
          } else if (
            validationRules?.pattern &&
            !validationRules.pattern.test(value)
          ) {
            isValid = false;
            newErrors[key] = "Invalid format";
          }
        });
    
        setErrors(newErrors);
    
        if (isValid) {
          onSubmit(formData);
        }
      };
    
      return { register, handleSubmit, errors };
    }
    
    function ExampleForm() {
      const { register, handleSubmit, errors } = useForm();
    
      const onSubmit = (data) => {
        console.log("Submitted data:", data);
      };
    
      return (
        <form onSubmit={handleSubmit(onSubmit)}>
          <div>
            <label>
              Name:
              <input {...register("name", { required: true })} />
            </label>
            {errors.name && <p>{errors.name}</p>}
          </div>
          <div>
            <label>
              Email:
              <input
                {...register("email", {
                  required: true,
                  pattern: /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/, // 이메일 유효성 검사
                })}
              />
            </label>
            {errors.email && <p>{errors.email}</p>}
          </div>
          <div>
            <label>
              Age:
              <input type="number" {...register("age", { required: true })} />
            </label>
            {errors.age && <p>{errors.age}</p>}
          </div>
          <div>
            <label>
              Subscribe to newsletter:
              <input type="checkbox" {...register("newsletter")} />
            </label>
          </div>
          <div>
            <label>
              Gender:
              <input
                type="radio"
                value="male"
                name="gender"
                {...register("gender", { required: true })}
              />
              Male
              <input
                type="radio"
                value="female"
                name="gender"
                {...register("gender", { required: true })}
              />
              Female
            </label>
            {errors.gender && <p>{errors.gender}</p>}
          </div>
          <div>
            <label>
              Country:
              <select {...register("country", { required: true })}>
                <option value="">Select Country</option>
                <option value="USA">USA</option>
                <option value="Canada">Canada</option>
              </select>
            </label>
            {errors.country && <p>{errors.country}</p>}
          </div>
          <button type="submit">Submit</button>
        </form>
      );
    }
    
    export default ExampleForm;

     

    반응형