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

#React

제어 컴포넌트 방식

제어 컴포넌트(controlled component)는 입력 값을 리액트 컴포넌트의 상태로 완전히 관리하는 방식입니다. 즉, 사용자가 입력한 값을 바로 DOM에 저장하지 않고 먼저 상태에 저장한 뒤 그 값을 화면에 표시하는 구조입니다.

제어 컴포넌트 방식은 리액트의 상태(state)를 폼 요소의 value와 연결해서 폼을 제어하는 것이다. 폼 요소의 값이 바뀌면 리액트는 바뀐 값을 확인하고 컴포넌트를 재렌더링한다.

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

  1. useState로 상태 생성
  2. 폼 요소의 value 속성과 상태 연결
  3. 폼 요소의 onChange 속성과 상태 업데이트 메서드 연결

text

제어 컴포넌트 방식으로 text(input 태그의 text 타입)를 사용하는 예시이다. 값을 변경할 때 마다 화면에 출력되는 값도 바뀌는 것을 확인할 수 있다.

import { useState } from 'react'

export default function App() {
	const [ value, setValue ] = useState('');
	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		setValue(e.target.value);
	};
	return (
		<>
			<form>
				<h1>input: {value}</h1>
				<input type="text" value={value} onChange={handleChange}></input>				
			</form>
		</>
	);
}

input 태그의 onChange에는 바로 상태 업데이트 메서드(setValue)를 넣을 수 없다. 현재 이벤트가 가지고 있는 값을 넣어야하기 때문에, 별도로 이벤트 핸들러를 설정하여 넣어준다(인라인도 가능). onChange가 반환하는 값은 이벤트 객체(e)이기 때문에 문자열 값(e.target.value)를 가지고 와 상태 업데이트 메서드를 호출한다.

제어 컴포넌트의 데이터 흐름

  1. 사용자가 입력을 시도함.
  2. onChange 이벤트 발생.
  3. 이벤트 핸들러가 실행되며 setValue를 통해 리액트 상태 업데이트.
  4. 상태가 변했으므로 컴포넌트가 재렌더링됨.
  5. 업데이트된 value가 다시 <input>의 값으로 주입됨.

이벤트 핸들러를 설정할 때 매개변수 타입을 쉽게 가지고 오는 방법

  1. onChange 속성에서 매개변수를 넣어 빈 화살표 함수를 만든다.
  2. 매개변수 위에 커서를 대면 이벤트 타입을 확인할 수 있다.

text를 여러개 사용할 때는 상태를 객체로 사용하면 useState를 여러번 사용하지 않아도 된다. 이 때, e.target.name으로 현재 바뀌고 있는 값을 확인하여 상태에 저장해준다.

import { useState } from 'react'

export default function App() {
	const [ formState, setFormState ] = useState({
		id: '', password: '', date: ''
	});
	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		setFormState((formState) => ({
			...formState,
			[e.target.name]: e.target.value,
		}));
	};
	return (
		<>
			<form>
				<h1>id: {formState.id} password: {formState.password} date: {formState.date}</h1>
				<input type="text" name="id" value={formState.id} onChange={handleChange}></input>
				<input type="password" name="password" value={formState.password} onChange={handleChange}></input>
				<input type="date" name="date" value={formState.date} onChange={handleChange}></input>				
			</form>
		</>
	);
}

setFormState() 내부의 화살표 함수에서 오른쪽 중괄호를 괄호로 한 번 더 감쌌다(괄호로 감싸지 않으면 에러 발생). 찾아보니 화살표 함수는 사용하는 두 가지 방식이 있는데, 중괄호를 가리키면 로직을 실행하는 것이고, 괄호를 가리키면 해당 값을 return 해 주는 것이라고 한다. 여기에서는 객체를 return 해 주어야 하므로 중괄호(객체)를 괄호로 감싼 형태가 되었다.

checkbox

checkbox(input 태그의 checkbox 타입)는 text와 사용법이 거의 같다. 차이점은 type이 ‘checkbox’인 것, 값이 문자열이 아닌 boolean인 점과 폼 요소의 속성을 value가 아닌 checked를 사용한다는 점이 있다.

import { useState } from 'react'

export default function App() {
	const [ formState, setFormState ] = useState({
		agree1: false, agree2: false, agree3: false
	});
	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		setFormState((formState) => ({
			...formState,
			[e.target.name]: e.target.checked,
		}));
	};
	return (
		<>
			<form>
				<label htmlFor="agree1">agree1({ formState.agree1 ? 'selected' : 'deselected' })</label>
				<input type="checkbox" name="agree1" id="agree1" checked={formState.agree1} onChange={handleChange}></input>
				<label htmlFor="agree2">agree2({ formState.agree2 ? 'selected' : 'deselected' })</label>
				<input type="checkbox" name="agree2" id="agree2" checked={formState.agree2} onChange={handleChange}></input>
				<label htmlFor="agree3">agree3({ formState.agree3 ? 'selected' : 'deselected' })</label>
				<input type="checkbox" name="agree3" id="agree3" checked={formState.agree3} onChange={handleChange}></input>				
			</form>
		</>
	);
}

<label> 태그에서 사용한 htmlFor 속성은 HTML의 for 속성에 대응되는 것으로, 자바스크립트의 반복문 예약어인 for가 같아 비슷한 말로 대체되었다. <label> 태그의 htmlFor과 <input> 태그의 id를 같게 맞추면, 라벨을 클릭했을 때도 이어진 폼 요소로 커서가 옮겨져 편리하다. htmlFor를 쓰지 않고 <label> 태그에 <input> 태그를 넣어 자동으로 연결하는 방법도 있다.

radio

radio(input 태그의 radio 타입)는 폼 요소가 여러개라도 값은 하나이므로, radio 그룹 당 하나의 useState만 사용하면 된다. type은 ‘radio’로, 값은 문자열로, 폼 요소의 속성은 value로 사용한다.

import { useState } from 'react'

export default function App() {
	const [ value, setValue ] = useState('red');
	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		setValue(e.target.value);
	};
	return (
		<>
			<form>
				<label htmlFor="red">red</label>
				<input type="radio" id="red" value="red" checked={value == "red"} onChange={handleChange}></input>
				<label htmlFor="blue">blue</label>
				<input type="radio" id="blue" value="blue" checked={value == "blue"} onChange={handleChange}></input>
							
			</form>
		</>
	);
}

radio 그룹을 여러개 사용할 때도 마찬가지로 객체를 사용하면 된다.

import { useState } from 'react'

export default function App() {
	const [ formState, setFormState ] = useState({
		gender: 'male', color: ''
	});
	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		setFormState((formState) => ({
			...formState,
			[e.target.name]: e.target.value,
		}));
	};
	return (
		<>
			<form>
				<div>
					<label htmlFor="male">male</label>
					<input type="radio" name="gender" id="male" value="male" checked={formState.gender == "male"} onChange={handleChange}></input>
					<label htmlFor="blue">female</label>
					<input type="radio" name="gender" id="female" value="female" checked={formState.gender == "female"} onChange={handleChange}></input>
				</div>
				<div>
					<label htmlFor="red">red</label>
					<input type="radio" name="color" id="red" value="red" checked={formState.color == "red"} onChange={handleChange}></input>
					<label htmlFor="blue">blue</label>
					<input type="radio" name="color" id="blue" value="blue" checked={formState.color == "blue"} onChange={handleChange}></input>
				</div>		
			</form>
		</>
	);
}

textarea

여러줄을 입력하고 싶을 때는 textarea 태그를 사용한다. 사용법은 태그를 제외하면 text와 같다.

import { useState } from 'react'

export default function App() {
	const [ formState, setFormState ] = useState({
		desc: '', introduce: ''
	});
	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		setFormState((formState) => ({
			...formState,
			[e.target.name]: e.target.value,
		}));
	};
	return (
		<>
			<form>
				<h1>desc: {formState.desc} introduce: {formState.introduce}</h1>
				<textarea name="desc" value={formState.desc} onChange={handleChange}></textarea>
				<textarea name="introduce" value={formState.introduce} onChange={handleChange}></textarea>
			</form>
		</>
	);
}