폼 다루기 - 비제어 컴포넌트 방식

#React

비제어 컴포넌트 방식

비제어 컴포넌트(unControlled component)는 폼 요소의 입력 값을 리액트 상태가 아닌 DOM 자체에서 직접 관리하는 방식입니다. 즉, 사용자가 입력한 값은 컴포넌트의 상태로 저장하지 않고, DOM에 그대로 저장합니다.

비제어 컴포넌트 방식은 useRef를 사용해 dom의 ref에 직접 접근해서 값을 가져와 폼을 제출하는 방식을 말한다. 상태를 리액트가 관리하지 않고 DOM 자체에서 관리하도록 내버려 두기 때문에 비제어라고 부른다.

useRef 훅은 useState 훅과 비슷한 리액트의 상태 훅이다. 하지만 useState 처럼 값을 실시간으로 확인하여 재렌더링하지 않고, dom 요소의 값을 참조할 때만 사용한다. useRef를 사용할 때는 useState와 달리 <> 안에 타입을 꼭 적어준다. 보통 초깃값 설정을 null로 하므로 오류를 방지하기 위해서이다.

비제어 컴포넌트 방식에서 값을 확인하는 순간은 대개 <form> 태그의 onSubmit()이 호출될 때이다. onSubmit() 말고도 다른 이벤트(ex. 클릭 이벤트) 에서도 코드를 작성하면 개발자가 참조한 값을 가져올 수 있다.

비제어 컴포넌트 방식을 사용하는 방법

  1. useRef 훅으로 참조 생성
  2. 입력 요소에 ref 객체 연결

text

text(input 태그의 text 타입)를 사용하는 예시는 다음과 같다.

import { useRef } from 'react'

export default function App() {
	const inputRef = useRef<HTMLInputElement>(null);
	const handleSubmit = (e: React.FormEvent) => {
		e.preventDefault();
		const inputData = inputRef.current?.value;
		alert(inputData);
	}
	return(
		<>
			<form onSubmit={handleSubmit}>
				<input type="text" ref={inputRef}></input>
				<button type="submit">Submit</button>
			</form>
		</>
	);
}

input.current?.value?옵셔널 체이닝이라고 하는 타입스크립트의 문법으로, 해당 값이 null인지 아닌지 확신할 수 없을 때 사용한다. 값이 null일 경우 undefined를 반환한다.

폼을 제출할 때가 아니라, 다른 버튼을 클릭할 때 useRef로 참조한 값을 가지고 오고 싶다면 다음과 같이 이벤트를 연결하면 된다.

import { useRef } from 'react'

export default function App() {
	const inputRef = useRef<HTMLInputElement>(null);
	const handleSubmit = (e: React.FormEvent) => {
		e.preventDefault();
		const inputData = inputRef.current?.value;
		alert(inputData);
	}
	const handleClick = () => {
		const inputData = inputRef.current?.value;
		alert(inputData);
	}
	return(
		<>
			<form onSubmit={handleSubmit}>
				<input type="text" ref={inputRef}></input>
				<button type="submit">Submit</button>
				<button type="button" onClick={handleClick}>Click</button>
			</form>
		</>
	);
}

useRef는 useState와 달리 여러개의 요소를 사용할 때 객체로 묶지 않고 따로따로 useRef를 정의한다. 객체로 묶는 것이 오히려 더 복잡하기 때문이다.

import { useRef } from 'react'

export default function App() {
	const idRef = useRef<HTMLInputElement>(null);
	const passwordRef = useRef<HTMLInputElement>(null);
	const dateRef = useRef<HTMLInputElement>(null);
	const handleSubmit = (e: React.FormEvent) => {
		e.preventDefault();
		const id = idRef.current?.value;
		const password = passwordRef.current?.value;
		const date = dateRef.current?.value;
		alert(`id: ${id} password: ${password} date: ${date}`);
	}
	return(
		<>
			<form onSubmit={handleSubmit}>
				<input type="text" ref={idRef}></input>
				<input type="password" ref={passwordRef}></input>
				<input type="date" ref={dateRef}></input>
				<button type="submit">Submit</button>
			</form>
		</>
	);
}

checkbox

제어 컴포넌트와 마찬가지로 checkbox(input 태그의 checkbox 타입)는 text와 사용법이 거의 같다. type을 ‘checkbox’로 지정하고, value 대신 checked를 사용해주면 된다.

import { useRef } from 'react'

export default function App() {
	const agree1Ref = useRef<HTMLInputElement>(null);
	const agree2Ref = useRef<HTMLInputElement>(null);
	const agree3Ref = useRef<HTMLInputElement>(null);
	const handleSubmit = (e: React.FormEvent) => {
		e.preventDefault();
		const agree1 = agree1Ref.current?.checked;
		const agree2 = agree2Ref.current?.checked;
		const agree3 = agree3Ref.current?.checked;
		alert(`agree1: ${agree1} agree2: ${agree2} agree3: ${agree3}`);
	}
	return(
		<>
			<form onSubmit={handleSubmit}>
				<label htmlFor="agree1">agree1</label>
				<input type="checkbox" id="agree1" ref={agree1Ref}></input>
				<label htmlFor="agree2">agree2</label>
				<input type="checkbox" id="agree2"  ref={agree2Ref}></input>
				<label htmlFor="agree3">agree3</label>
				<input type="checkbox" id="agree3"  ref={agree3Ref}></input>
				<button type="submit">Submit</button>
			</form>
		</>
	);
}

radio

radio(input 태그의 radio 타입)를 사용할 때는 useRef를 사용하지 않고, FormData를 사용한다. 왜냐하면 radio 그룹은 여러 개의 폼 요소가 있어도 논리적으로 하나의 값을 가리키는데, 각각의 폼 요소에 useRef를 설정해서 checked를 확인하는 것은 번거롭기 때문이다. form을 인자로 받아 FormData 객체를 생성하면, 해당 form의 요소의 키-값 쌍을 반환받을 수 있다. 같은 그룹의 radio 버튼의 name 속성을 모두 같게 하면, FormData의 get() 메서드를 이용해서 현재 선택된 라디오의 값을 확인할 수 있다.

export default function App() {
	const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
		e.preventDefault();
		const formData = new FormData(e.currentTarget)
		alert(formData.get('color'));
	}
	return(
		<>
			<form onSubmit={handleSubmit}>
				<label>
					Red
					<input type="radio" name="color" value="red" defaultChecked}></input>
				</label>
				<label>
					Blue
					<input type="radio" name="color" value="blue"></input>
				</label>
				<button type="submit">Submit</button>
			</form>
		</>
	);
}

defaultChecked는 라디오 그룹에서 기본으로 하나를 선택 해 주는 속성이다. defaultChecked를 설정하지 않으면 사용자가 라디오 버튼을 하나도 선택하지 않고 폼을 제출할 수 있어, 꼭 하나를 설정 해 주어야 한다.

만약 onSubmit이 아닌 다른 클릭 이벤트에서 radio 값을 확인하려는 경우, form에 useRef를 사용한다.

import { useRef } from 'react'

export default function App() {
	const formRef = useRef<HTMLFormElement>(null);
	const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
		e.preventDefault();
		const formData = new FormData(e.currentTarget)
		alert(formData.get('color'));
	}
	const handleClick = () => {
		if (formRef.current) {
			const formData = new FormData(formRef.current)
			alert(formData.get('color'));
		}
	}
	return(
		<>
			<form ref={formRef} onSubmit={handleSubmit}>
				<label>
					Red
					<input type="radio" name="color" value="red" defaultChecked></input>
				</label>
				<label>
					Blue
					<input type="radio" name="color" value="blue"></input>
				</label>
				<button type="submit">Submit</button>
				<button type="button" onClick={handleClick}>Click</button>
			</form>
		</>
	);
}

여러개의 radio 그룹이 있을 때는 name을 사용해서 서로 구분해준다.

export default function App() {
	const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
		e.preventDefault();
		const formData = new FormData(e.currentTarget)
		console.log(formData.get('gender'));
		console.log(formData.get('color'));
	}
	return(
		<>
			<form onSubmit={handleSubmit}>
				<div>
					<label>
						Male
						<input type="radio" name="gender" value="male" defaultChecked></input>
					</label>
					<label>
						Female
						<input type="radio" name="gender" value="female"></input>
					</label>
				</div>
				<div>
					<label>
						Red
						<input type="radio" name="color" value="red" defaultChecked></input>
					</label>
					<label>
						Blue
						<input type="radio" name="color" value="blue"></input>
					</label>
				</div>
				<button type="submit">Submit</button>
			</form>
		</>
	);
}

textarea

여러줄의 text를 비제어 방식으로 가지고 오고 싶다면, useState와 마찬가지로 textarea 태그를 사용하면 된다. 각각의 textarea를 useRef로 선언하며, useRef의 자료형은 HTMLTextAreaElement를 사용한다.

import { useRef } from 'react'

export default function App() {
	const descRef = useRef<HTMLTextAreaElement>(null);
	const introduceRef = useRef<HTMLTextAreaElement>(null);
	const handleSubmit = (e: React.FormEvent) => {
		e.preventDefault();
		const desc = descRef.current?.value;
		const introduce = introduceRef.current?.value;
		console.log(desc);
		console.log(introduce);
	}
	return(
		<>
			<form onSubmit={handleSubmit}>
				<textarea ref={descRef}></textarea>
				<textarea ref={introduceRef}></textarea>
				<button type="submit">Submit</button>
			</form>
		</>
	);
}