[React] useRef vs useState 중 무엇을 써야하지?
목차
근본적인 질문에 대해 구체적인 것이 아니라 모호하게 알고 있어 이참에 정리하고자 한다.
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;
++ref의 타입
useRef를 사용하다보면 인자로 어떨 때 null을 넣어야할지? 비어둘지?
useRef에는 3가지 오버로딩이 존재한다.
1. 인자: [초기값] => 리턴: MutableRefObject<T>;
2. 인자: [초기값 | null] => 리턴: RefObject<T>;
3. 인자: [] => 리턴: MutableRefObject<T | undefined>;
위를 보게 되면 총 2개의 타입이 존재한다. MutableRefObject과 RefObject다.
✔️ useRef는 .current 프로퍼티에 변경 가능한 값을 담고 있는 “상자” 📦
인수를 .current에 저장하게 된다.
아래 두 개의 리턴타입은 .current 프로퍼티를 직접 수정 가능 여부에 따라 구분
✔️ MutableRefObject<T>
직접 수정 가능 ⭕️
✔️ MutableRefObject<T | undefined>
직접 수정 불가능 ❌, 다만 undefined이 아님이 체크되면 가능 ⭕️
✔️ RefObject<T>
직접 수정 불가능 ❌
즉, 특정 초기값 혹은 비어두게 되면 current를 직접 수정 가능하며, null을 부여할 경우 current를 직접 수정 불가능하게 된다. 쉽게 말해 null로 부여할 경우 아래와 같은 경우에 에러가 발생한다.
이때 RefObject는 속성값은 바꿀 수 있다.
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (divRef.current) {
divRef.current.style.backgroundColor = 'tomato';
}
}, []);
단 아래와 같은 경우는 안된다.
divRef.current = document.createElement('div'); // ❌ readonly라 불가