폼 다루기 - 제어 컴포넌트 방식
제어 컴포넌트 방식
제어 컴포넌트(controlled component)는 입력 값을 리액트 컴포넌트의 상태로 완전히 관리하는 방식입니다. 즉, 사용자가 입력한 값을 바로 DOM에 저장하지 않고 먼저 상태에 저장한 뒤 그 값을 화면에 표시하는 구조입니다.
제어 컴포넌트 방식은 리액트의 상태(state)를 폼 요소의 value와 연결해서 폼을 제어하는 것이다. 폼 요소의 값이 바뀌면 리액트는 바뀐 값을 확인하고 컴포넌트를 재렌더링한다.
제어 컴포넌트 방식을 사용하는 방법
- useState로 상태 생성
- 폼 요소의 value 속성과 상태 연결
- 폼 요소의 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)를 가지고 와 상태 업데이트 메서드를 호출한다.
제어 컴포넌트의 데이터 흐름
- 사용자가 입력을 시도함.
- onChange 이벤트 발생.
- 이벤트 핸들러가 실행되며 setValue를 통해 리액트 상태 업데이트.
- 상태가 변했으므로 컴포넌트가 재렌더링됨.
- 업데이트된 value가 다시
<input>의 값으로 주입됨.
이벤트 핸들러를 설정할 때 매개변수 타입을 쉽게 가지고 오는 방법
- onChange 속성에서 매개변수를 넣어 빈 화살표 함수를 만든다.
- 매개변수 위에 커서를 대면 이벤트 타입을 확인할 수 있다.

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>
</>
);
}
