리액트에서는 주로 인풋의 값을 state로 관리한다.
state 값과 인풋의 값을 동일하게 만드는게 핵심
리액트에서 onChange는 html의 onInput처럼 사용자가 값을 입력할 때마다 onChange 이벤트가 발생한다.
(html과 다른 점)
import { useState } from "react";
import "./ReviewForm.css";
function ReviewForm() {
const [title, setTitle] = useState("");
const [rating, setRating] = useState(0);
const [content, setContent] = useState("");
const handleTitleChange = (e) => {
setTitle(e.target.value);
};
const handleRatingChange = (e) => {
const nextRating = Number(e.target.value);
setRating(nextRating);
};
const handleContentChange = (e) => {
setContent(e.target.value);
};
return (
<form className="ReviewForm">
<input value={title} onChange={handleTitleChange} />
<input type="number" value={rating} onChange={handleRatingChange} />
<textarea value={content} onChange={handleContentChange} />
</form>
);
}
export default ReviewForm;
태그에서 사용하는 속성인 for 는 자바스크립트 반복문 키워드인 for 와 겹치기 때문에 리액트에서는 htmlFor 를 사용한다.
const handleSubmit = (e) => {
e.preventDefault();
console.log({
title,
rating,
content,
});
};
submit 버튼을 눌렀을 때 기본 동작은 submit과 함께 getRequest를 보내는 것이다. (페이지 이동)
그렇기 때문에 e.preventDefault();
를 통해 랜더링을 막는다.
import { useState } from 'react';
import './ReviewForm.css';
function ReviewForm() {
const [values, setValues] = useState({
title: '',
rating: 0,
content: '',
});
const handleChange = (e) => {
const { name, value } = e.target;
setValues((prevValues) => ({
...prevValues,
[name]: value,
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(values);
};
return (
<form className="ReviewForm" onSubmit={handleSubmit}>
<input name="title" value={values.title} onChange={handleChange} />
<input type="number" name="rating" value={values.rating} onChange={handleChange} />
<textarea name="content" value={values.content} onChange={handleChange} />
<button type="submit">확인</button>
</form>
);
}
export default ReviewForm;
깔끔한 코드를 작성할 수 있다.
인풋의 value 값을 리액트에서 지정
리액트에서 사용하는 값과 실제 인풋 value 의 값이 항상 일치
값을 예측하기가 쉽고 인풋에 쓰는 값을 여러 군데서 쉽게 바꿀 수 있다는 장점이 있어서 주로 권장되는 방법
이때 State냐 Prop이냐는 중요하지 않고, 리액트로 value 를 지정한다는 것이 핵심
예시
function TripSearchForm() {
const [values, setValues] = useState({
location: 'Seoul',
checkIn: '2022-01-01',
checkOut: '2022-01-02',
})
const handleChange = (e) => {
const { name, value } = e.target;
setValues((prevValues) => ({
...prevValues,
[name]: value,
}));
}
return (
<form>
<h1>검색 시작하기</h1>
<label htmlFor="location">위치</label>
<input id="location" name="location" value={values.location} placeholder="어디로 여행가세요?" onChange={handleChange} />
<label htmlFor="checkIn">체크인</label>
<input id="checkIn" type="date" name="checkIn" value={values.checkIn} onChange={handleChange} />
<label htmlFor="checkOut">체크아웃</label>
<input id="checkOut" type="date" name="checkOut" value={values.checkOut} onChange={handleChange} />
<button type="submit">검색</button>
</form>
)
}
function TripSearchForm({ values, onChange }) {
return (
<form>
<h1>검색 시작하기</h1>
<label htmlFor="location">위치</label>
<input id="location" name="location" value={values.location} placeholder="어디로 여행가세요?" onChange={onChange} />
<label htmlFor="checkIn">체크인</label>
<input id="checkIn" type="date" name="checkIn" value={values.checkIn} onChange={onChange} />
<label htmlFor="checkOut">체크아웃</label>
<input id="checkOut" type="date" name="checkOut" value={values.checkOut} onChange={onChange} />
<button type="submit">검색</button>
</form>
)
}
인풋의 value 값을 리액트에서 지정하지 않음
경우에 따라서 필요한 방법 (파일 인풋)
리액트에서 실제 인풋값을 제어하지 않는 경우
function TripSearchForm({ onSubmit }) {
return (
<form onSubmit={onSubmit} >
<h1>검색 시작하기</h1>
<label htmlFor="location">위치</label>
<input id="location" name="location" placeholder="어디로 여행가세요?" />
<label htmlFor="checkIn">체크인</label>
<input id="checkIn" type="date" name="checkIn" />
<label htmlFor="checkOut">체크아웃</label>
<input id="checkOut" type="date" name="checkOut" />
<button type="submit">검색</button>
</form>
)
}
위 코드에서 폼 태그를 참조하는 방법
const handleSubmit = (e) => {
e.preventDefault();
const form = e.target;
const location = form['location'].value;
const checkIn = form['checkIn'].value;
const checkOut = form['checkOut'].value;
// ....
}
const handleSubmit = (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
// ...
}
리액트에서 파일 인풋은 반드시 비제어 인풋으로 지정해야 한다.
function FileInput({ name, value, onChange }) {
const handleChange = (e) => {
const nextValue = e.target.files[0];
onChange(name, nextValue);
};
return <input type="file" onChange={handleChange} />;
}
export default FileInput;
const ref = useRef();
// ...
return <div ref={ref}>안녕 리액트!</div>;
const node = ref.current;
if (node) {
// node를 사용하는 코드
}
function FileInput({ name, value, onChange }) {
const inputRef = useRef();
const handleChange = (e) => {
const nextValue = e.target.files[0];
onChange(name, nextValue);
};
return <input type="file" onChange={handleChange} ref={inputRef} />;
}
export default FileInput;
ref로 실제 DOM 노드를 참조할 수 있다.
import { useRef } from 'react';
function FileInput({ name, value, onChange }) {
const inputRef = useRef();
const handleChange = (e) => {
const nextValue = e.target.files[0];
onChange(name, nextValue);
};
const handleClearClick = () => {
const inputNode = inputRef.current;
if (!inputNode) return;
inputNode.value = '';
onChange(name, null);
};
return (
<div>
<input type="file" onChange={handleChange} ref={inputRef} />
{value && <button onClick={handleClearClick}>X</button>}
</div>
);
}
export default FileInput;
import { useRef } from 'react';
// ...
const ref = useRef();
useRef 함수로 Ref 객체를 만들 수 있다.
const ref = useRef();
// ...
<div ref={ref}> ... </div>
ref Prop에다가 앞에서 만든 Ref 객체를 내려주면 된다.
const node = ref.current;
if (node) {
// node 를 사용하는 코드
}
Ref 객체의 current 라는 프로퍼티를 사용하면 DOM 노드를 참조할 수 있다.
current 값은 없을 수도 있으니까 반드시 값이 존재하는지 검사하고 사용해야 함!
파일 객체를 object Url로 만들면 된다.
import { useEffect, useRef, useState } from 'react';
function FileInput({ name, value, onChange }) {
const [preview, setPreview] = useState();
const inputRef = useRef();
const handleChange = (e) => {
const nextValue = e.target.files[0];
onChange(name, nextValue);
};
const handleClearClick = () => {
const inputNode = inputRef.current;
if (!inputNode) return;
inputNode.value = '';
onChange(name, null);
};
useEffect(() => {
if (!value) return;
const nextPreview = URL.createObjectURL(value);
setPreview(nextPreview);
}, [value]);
return (
<div>
<img src={preview} alt="이미지 미리보기" />
<input type="file" accept="image/png, image/jpeg" onChange={handleChange} ref={inputRef} />
{value && <button onClick={handleClearClick}>X</button>}
</div>
);
}
export default FileInput;
preview state 추가
메모리 할당을 해제하는 것 (revokeObjectURL 함수 사용)
import { useEffect, useRef, useState } from 'react';
function FileInput({ name, value, onChange }) {
const [preview, setPreview] = useState();
const inputRef = useRef();
const handleChange = (e) => {
const nextValue = e.target.files[0];
onChange(name, nextValue);
};
const handleClearClick = () => {
const inputNode = inputRef.current;
if (!inputNode) return;
inputNode.value = '';
onChange(name, null);
};
useEffect(() => {
if (!value) return;
const nextPreview = URL.createObjectURL(value);
setPreview(nextPreview);
return () => {
setPreview();
URL.revokeObjectURL(nextPreview);
};
}, [value]);
return (
<div>
<img src={preview} alt="이미지 미리보기" />
<input type="file" accept="image/png, image/jpeg" onChange={handleChange} ref={inputRef} />
{value && <button onClick={handleClearClick}>X</button>}
</div>
);
}
export default FileInput;
프로그래밍에서 사이드 이펙트는 외부에 부수적인 작용을 하는 것을 말한다.
(함수 안에서 함수 바깥에 있는 값이나 상태를 변경하는 것, ex)console.log)
useEffect는 리액트 컴포넌트 함수 안에서 사이드 이펙트를 실행하고 싶을 때 사용하는 함수이다.
주로 외부에 있는 데이터나 상태를 변경할 때 사용
useEffect는 동기화에 쓰면 유용한 경우가 많다. (컴포넌트 안에 데이터와 리액트 바같에 있는 데이터를 일치시키는 것)
useEffect(() => {
// 사이드 이펙트
return () => {
// 사이드 이펙트에 대한 정리
}
}, [dep1, dep2, dep3, ...]);
useEffect 의 콜백 함수에서 사이드 이펙트를 만들면 정리가 필요한 경우가 있다.
이럴 때 콜백 함수에서 리턴 값으로 정리하는 함수를 리턴할 수 있는데, 리턴한 정리 함수에서는 사이드 이펙트에 대한 뒷정리를 한다.
예를 들면 이미지 파일 미리보기를 구현할 때 Object URL을 만들어서 브라우저의 메모리를 할당(createObjectURL) 하고 정리 함수에서는 이때 할당한 메모리를 다시 해제(revokeObjectURL)한 경우가 있다.
콜백을 한 번 실행했으면 정리 함수도 반드시 한 번 실행된다.
정확히는 새로운 콜백 함수가 호출되기 전에 실행되거나 (앞에서 실행한 콜백의 사이드 이펙트를 정리), 컴포넌트가 화면에서 사라지기 전에 실행된다. (맨 마지막으로 실행한 콜백의 사이드 이펙트를 정리).