React 입력 폼 다루기
제어 컴포넌트
const [value, setValue] = useState('');
function handleChange(e) {
setValue(e.target.value);
}
<input type="text" onChange={handleChange} value={value}></input>
위의 예시를 살펴보면, 텍스트를 입력할 때마다 handleChange 함수를 통해 value state 값을 변경해주고, 이렇게 변경된 value는 value prop을 통해 다시 내려보내준다. 이처럼 state를 통해 input value 값을 관리하는 방식을 제어 컴포넌트라고 부른다.
제어 컴포넌트는 그 값을 예측하기가 쉽고 인풋에 쓰는 값을 여러 군데서 쉽게 바꿀 수 있다는 장점이 있어서 리액트에서 권장하는 방법인데, 이때 제어를 하는 주체가 State냐 Prop이냐는 중요하지 않고, 리액트로 value를 지정한다는 것이 핵심이다.
하나의 state로 form 구현
function ReviewForm() {
const [values, setValues] = useState({
title: '',
rating: 0,
content: '',
});
const handleChange = (e) => {
const { name, value } = e.target;
setValues((prevValues) => ({
...prevValues,
[name]: value,
}));
};
한 컴포넌트 안에 여러 인풋을 받는다면, 상황에 따라서는 커스텀 훅을 만들어 사용할 수도 있겠지만, 이렇게 객체를 만들어 하나의 state에서 관리할 수 있도록 만들 수도 있다.
file Input
파일 객체
input 태그의 타입으로 file을 주면 다양한 파일을 업로드할 수 있게 된다. accept="image/png, image/jpeg"와 같이 accept 속성을 주면 특정한 파일만을 가려서 업로드할 수 있게 제약을 걸 수 있고, multiple 속성을 주면 여러 파일을 동시에 받을 수도 있다. 이 경우 e.target.files로 접근하면 파일 객체를 확인할 수 있다.
{
name: "example.png",
size: 102403, // 파일 크기 (바이트 단위)
type: "image/png", // MIME 유형
lastModified: 1631535600000 // 마지막 수정 시간 (타임스탬프)
}
파일 객체는 유사 배열 형태이며 length 값은 쓸 수 있는 것 같다.
파일 추가
파일을 관리하는 input 태그에는 보안 문제로 인해 value prop을 지정할 수 없다. 따라서 파일을 받는 input 태그는 반드시 비제어 컴포넌트로 작성해야 하는 것이다.
function FileInput({ setFileValue }) {
const handleChange = (e) => {
const FileValue = e.target.files[0];
setFileValue(FileValue);
};
return <input type="file" onChange={handleChange} />;
}
// 상위 컴포넌트에서 FileInput 컴포넌트를 사용할 때
<FileInput onChange={setFileValue} />
살펴보면 인풋 value를 관리하는 fileValue state와 input 태그 사이에는 어떠한 직접적인 연관도 없다는 것을 알 수 있다.
useRef로 파일 인풋 초기화
const inputRef = useRef();
<input type="file" onChange={handleChange} ref={inputRef}/>
리액트가 제공하는 기본 훅 중 useRef를 사용하면 랜더링이 완료된 시점의 DOM node를 잡아올 수 있다. 이를 활용하여 파일 인풋을 초기화하는 함수를 만들 수 있다.
const handleClearClick = () => {
const inputNode = inputRef.current; // useRef로 노드를 잡아오기
if (!inputNode) return; // 잡아온 노드가 없으면 함수 바로 종료
inputNode.value = '' // 인풋 value를 초기화
setFileValue(null) // fileValue state도 초기화
};
return (
<div>
<input type="file" onChange={handleChange} ref={inputRef}/>
{value && <button type="button" onClick={handleClearClick}>
X
</button>}
</div>
);
파일 미리보기와 side effect
const [preview, setPreview] = useState();
useEffect(() => {
if (!value) return;
const nextPreview = URL.createObjectURL(value);
setPreview(nextPreview);
return () => {
setPreview();
URL.revokeObjectURL(nextPreview);
};
, [value]);
<img src={preview} alt="이미지 미리보기" />
프로그래밍에서 side effect란 함수나 표현식이 외부에 있는 상태를 변경하거나 외부와의 상호작용을 일으키는 것을 의미한다. 함수 내부에서 전역 변수의 값을 변경하거나, fetch를 하는 것도 side effect라고 할 수 있는 것이다. 따라서 어떤 함수가 side effect를 일으키지 않는다면 코드의 예측 가능성과 유지 보수성이 높아지게 될 것이다.
그러나 위에서 언급한 것처럼 fetch를 하게 되는 경우처럼 어쩔 수 없이 side effect를 사용해야 할 수도 있다. 파일 미리보기 기능을 만드는 것이 어쩔 수 없이 side effect를 사용해야 하는 또 다른 경우이다.
미리보기 기능을 만들기 위해서는 URL 객체의 createObjectURL 메소드를 통해 이미지 파일을 이미지 태그에서 사용할 수 있는 url 주소로 변경해줘야 한다. 그런데 createObjectURL 메소드를 통해 메모리 힙에 등록된 이 url 주소는 따로 끊어주지 않는 한 (함수의 클로저와 같이) 세션이 종료될 때까지 메모리 힙을 차지하고 있게 되는 것이다.
따라서 이미지 파일이 사용되지 않을 때마다 revokeObjectURL 메소드를 통해 연결을 끊어주어야 하는데, useEffect의 클린업 단계를 활용하면 좋다. useEffect의 클린업 함수는 useEffect의 콜백 함수가 리턴하는 함수이다. 이를 통해 preview state를 비워주고, 메모리 힙과의 연결도 끊어줄 수 있다.
이렇게 참조를 끊어주면 JS의 가비지 컬렉터가 알아서 메모리를 비워줄 것이다.
블로그의 정보
Ayden's journal
Beard Weard Ayden