1. 리액트에서 입력 폼 만들기
- 리액트에서는 주로 input의 값을 State로 관리
- State값과 input의 값을 동일하게 만듬
- 리액트에서의 onChange는 순수 자바스크립트에서 onChange와 다르게 동작
-> 사용자가 값을 입력할 때마다 onChange 이벤트 발생
ReviewForm.js
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;
App.js
return (
<div>
<div>
<button onClick={handleNewestClick}>최신순</button>
<button onClick={handleBestClick}>베스트순</button>
</div>
<ReviewForm />
<ReviewList items={sortedItems} onDelete={handleDelete} />
{hasNext && (<button disabled={isLoading} onClick={handleLoadMore}>더보기</button>)}
{loadingError?.message && <span>{loadingError.message}</span>}
</div>
);
2. onSubmit
- HTML form 태그의 기본 동작은 submint 버튼을 눌렀을 때 입력 폼의 값과 함께 GET 리퀘스트를 보내는 것
- 이러한 기본동작을 막아줘야 함
- 이벤트 객체의 preventDefault 함수 사용
ReviewForm.js
const handleSubmit = (e) => {
e.preventDefault();
console.log({
title,
rating,
content,
});
};
return (
<form className="ReviewForm" onSubmit={handleSubmit}>
<input value={title} onChange={handleTitleChange} />
<input type="number" value={rating} onChange={handleRatingChange} />
<textarea value={content} onChange={handleContentChange}/>
<button type="submit">확인</button>
</form>
3. 하나의 state로 폼 구현
- 이벤트 객체에서 name값을 가져 올 수 있다는 점을 활용
ReviewForm.js
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;
4. 제어 컴포넌트와 비제어 컴포넌트
제어 컴포넌트(Controlled Component)
- input의 value값을 리액트에서 지정
- input의 value와 onChange 핸들러를 사용하면 input의 값 제어 가능
- 리액트에서 사용하는 값과 실제 input의 값이 항상 일치하기 때문에 동작 예측이 쉽고 input값을 여러 군데에서 쉽게 바꿀 수 있음
- 주로 권장되는 방법
비제어 컴포넌트(Uncontrolled Component)
- input의 value 값을 리액트에서 지정하지 않음
-리액트에서 실제 input값을 제어하지 않음
- 경우에 따라 사용
5. 입력 폼 정리
- 리액트에선 순수 HTML과 다르게 onChange Prop을 사용하면 입력 값이 바뀔 때마다 핸들러 함수를 실행
1) 폼을 다루는 기본적인 방법
- 스테이트를 만들고 target.value값을 사용해서 값을 변경해 줄 수 있음
- value Prop으로 스테이트 값을 내려주고, onChange Prop으로 핸들러 함수를 넘겨줌
function TripSearchForm() {
const [location, setLocation] = useState('Seoul');
const [checkIn, setCheckIn] = useState('2022-01-01');
const [checkOut, setCheckOut] = useState('2022-01-02');
const handleLocationChange = (e) => setLocation(e.target.value);
const handleCheckInChange = (e) => setCheckIn(e.target.value);
const handleCheckOutChange = (e) => setCheckOut(e.target.value);
return (
<form>
<h1>검색 시작하기</h1>
<label htmlFor="location">위치</label>
<input id="location" name="location" value={location} placeholder="어디로 여행가세요?" onChange={handleLocationChange} />
<label htmlFor="checkIn">체크인</label>
<input id="checkIn" type="date" name="checkIn" value={checkIn} onChange={handleCheckInChange} />
<label htmlFor="checkOut">체크아웃</label>
<input id="checkOut" type="date" name="checkOut" value={checkOut} onChange={handleCheckOutChange} />
<button type="submit">검색</button>
</form>
)
}
2) 폼 값을 객체 하나로 처리
- 이벤트 객체의 target.name과 target.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>
)
}
3) 기본 submit 동작 막기
- HTML 폼의 기본 동작은 submit 타입의 버튼을 눌렀을 때 페이지를 이동하는 것
- 이벤트 객체의 preventDefault를 사용하면 이 동작을 막을 수 있음
const handleSubmit = (e) => {
e.preventDefault();
// ...
}
6. 파일 인풋
- 파일 인풋은 반드시 비제어 input으로 만들어야 함
- 파일 인풋에서는 이벤트 객체의 target,value값이 아니라 target.files를 사용
- handleChange() 라는 함수를 만들고, 첫 번째 파일을 nextValue라는 변수로 지정한 다음 이걸 onChange() 함수로 name과 함께 넘겨주면 됨
FileInput.js
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;
ReviewForm.js
import { useState } from 'react';
import FileInput from './FileInput';
import './ReviewForm.css';
function ReviewForm() {
const [values, setValues] = useState({
title: '',
rating: 0,
content: '',
imgFile: null,
});
const handleChange = (name, value) => {
setValues((prevValues) => ({
...prevValues,
[name]: value,
}));
};
const handleInputChange = (e) => {
const { name, value } = e.target;
handleChange(name, value);
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(values);
};
return (
<form className="ReviewForm" onSubmit={handleSubmit}>
<FileInput name="imgFile" value={values.imgFile} onChange={handleChange} />
<input name="title" value={values.title} onChange={handleInputChange} />
<input type="number" name="rating" value={values.rating} onChange={handleInputChange} />
<textarea name="content" value={values.content} onChange={handleInputChange} />
<button type="submit">확인</button>
</form>
);
}
export default ReviewForm;
7. ref로 DOM 노드 가져오기
- ref prop
- 원하는 시점에 실제 DOM 노드에 접근하고 싶을 때 사용할 수 있는 prop
- ref를 쓰면 실제 DOM 노드를 직접 참조 가능
FileInput.js
import { useEffect, useRef } from 'react';
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;
8. 파일 인풋 초기화
- FileInput의 value 속성은 사용자만 직접 바꿀 수 있고 자바스크립트로 바꿀 때는 빈 문자열로만 바꿀 수 있음
- value 속성을 빈 문자열로 바꿔주면 선택한 파일이 초기화
- 파일 인풋 노드를 참조할 Ref 객체 생성( useRef() 함수 사용)
- inputRef 객체를 파일 인풋에 ref Prop으로 내려줌
- handleClearClick 함수에선 인풋 노드에 해당하는 inputRef.current 값이 있는지 확인하고, DOM 노드의 value 값을 빈 문자열로 변경
FileInput.js
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;
9. ref와 useRef
1) Ref 객체 생성
- useRef 함수로 Ref 객체 생성
import { useRef } from 'react';
// ...
const ref = useRef();
2) ref Prop 사용
- ref Prop에다가 앞에서 만든 Ref 객체를 내려줌
const ref = useRef();
// ...
<div ref={ref}> ... </div>
3) Ref 객체에서 DOM 노드 참조
- Ref 객체의 current라는 프로퍼티를 사용하면 DOM 노드를 참조할 수 있음
- current 값은 없을 수도 있으니까 반드시 값이 존재하는지 검사하고 사용해야 함
const node = ref.current;
if (node) {
// node 를 사용하는 코드
}
9. 이미지 파일 미리보기
- 파일 객체를 Object-URL로 만들면 파일에 대한 주소를 만들 수 있음
- ObjectURL은 URL.createObjectURL이라는 함수로 만들 수 있음
- ObjectURL을 만들면 웹 브라우저는 메모리를 할당하고 파일에 해당하는 주소를 만들어 줌
- 사이드 이펙트(side effect) : 컴포넌트 함수에서 외부의 상태를 바꿈
- 리액트에서는 사이드 이펙트를 다루는 경우에 주로 useEffect를 사용
FileInput.js
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" onChange={handleChange} ref={inputRef} />;
{value && <button onClick={handleClearClick}>X</button>}
</div>
);
}
export default FileInput;
10. 사이트 이펙트 정리하기
- 파일을 선택할 때마다 메모리를 할당하기만 한다면 메모리가 낭비됨
- 다른 파일을 선택하거나 파일 선택을 해제했을 때 메모리도 같이 해제해야함
- revokeObjectURL로 메모리 할당을 해제
- useEffect의 콜백 함수에서 정리함수를 리턴하면 objectURL을 더이상 사용하지 않을 때 해제 가능
FileInput.js
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;
11. 사이드 이펙트와 useEffect
1) 사이드 이펙트
- 외부에 부수적인 자굑을 하는 것
- 함수 안에서 함수 바깥에 있는 값이나 상태를 변경하는 것
let count = 0;
function add(a, b) {
const result = a + b;
count += 1; // 함수 외부의 값을 변경
return result;
}
const val1 = add(1, 2);
const val2 = add(-4, 5);
- add 함수는 실행하면서 함수 외부의 상태 (count 변수)가 바뀌기 때문에, 이런 함수를 "사이드 이펙트가 있다"고 함
2) 사이드 이펙트와 useEffect
- useEffect는 리액트 컴포넌트 함수 안에서 사이드 이펙트를 실행하고 싶을 때 사용하는 함수
ex) DOM 노드 직접 변경, 브라우저에 데이터를 저장하고 네트워크 리퀘스트를 보내는 것
2-1) 페이지 정보 변경
useEffect(() => {
document.title = title; // 페이지 데이터를 변경
}, [title]);
2-2) 네트워크 요청
useEffect(() => {
fetch('https://example.com/data') // 외부로 네트워크 리퀘스트
.then((response) => response.json())
.then((body) => setData(body));
}, [])
2-3) 데이터 저장
useEffect(() => {
localStorage.setItem('theme', theme); // 로컬 스토리지에 테마 정보를 저장
}, [theme]);
2-4) 타이머
useEffect(() => {
const timerId = setInterval(() => {
setSecond((prevSecond) => prevSecond + 1);
}, 1000); // 1초마다 콜백 함수를 실행하는 타이머 시작
return () => {
clearInterval(timerId);
}
}, []);
3) useEffect를 쓰면 좋은 경우
- useEffect는 '동기화'에 쓰면 유용
- 동기화는 컴포넌트 안에 데이터와 리액트 바깥에 있는 데이터를 일치시키는 것
import { useEffect, useState } from 'react';
const INITIAL_TITLE = 'Untitled';
function App() {
const [title, setTitle] = useState(INITIAL_TITLE);
const handleChange = (e) => {
const nextTitle = e.target.value;
setTitle(nextTitle);
};
const handleClearClick = () => {
setTitle(INITIAL_TITLE);
};
useEffect(() => {
document.title = title;
}, [title]);
return (
<div>
<input value={title} onChange={handleChange} />
<button onClick={handleClearClick}>초기화</button>
</div>
);
}
export default App;
- document를 다루는 사이드 이펙트 부분만 따로 처리
- setTitle 함수를 쓸 때마다 document.title을 변경하는 코드를 신경쓰지 않아도 됨
4) 정리 함수(Cleanup Function)
- useEffect의 콜백 함수에서 사이드 이펙트를 만들면 정리가 필요한 경우가 있음
- 이럴 때 콜백 함수에서 리턴 값으로 정리하는 함수 리턴 가능
useEffect(() => {
// 사이드 이펙트
return () => {
// 사이드 이펙트에 대한 정리
}
}, [dep1, dep2, dep3, ...]);
4-1) 정리 함수가 실행되는 시점
- 콜백을 한 번 실행했으면, 정리 함수도 반드시 한 번 실행됨
- 새로운 콜백 함수가 호출되기 전에 실행되거나(앞에서 실행한 콜백의 사이드 이펙트를 정리), 컴포넌트가 화면에서 사라지기전에 실행(맨 마지막으로 실행한 콜백의 사이드 이펙트를 정리)
12. 별점 컴포넌트, 별점 인풋 만들기
- Star 컴포넌트는 별 하나를 보여주는 컴포넌트
- Rating 컴포넌트는 별 다섯개를 보여주는 컴포넌트
- 별점을 클릭했을 때는 onSelect 함수가 해당하는 별점 값으로 실행
- 별점에 마우스를 올렸을 때는 onHover 함수가 해당하는 별점값으로 실행
- 마우스가 영역을 벗어나면 onMouseOut을 실행
Rating.js
import './Rating.css';
const RATINGS = [1, 2, 3, 4, 5];
function Star({ selected = false, rating = 0, onSelect, onHover }) {
const className = `Rating-star ${selected ? 'selected' : ''}`;
const handleClick = onSelect ? () => onSelect(rating) : undefined;
const handleMouesOver = onHover ? () => onHover(rating) : undefined;
return (
<span
className={className}
onClick={handleClick}
onMouseOver={handleMouesOver}
>
★
</span>
);
}
function Rating({ className, value = 0, onSelect, onHover, onMouseOut }) {
return (
<div className={className} onMouseOut={onMouseOut}>
{RATINGS.map((rating) => (
<Star
key={rating}
selected={value >= rating}
rating={rating}
onSelect={onSelect}
onHover={onHover}
/>
))}
</div>
);
}
export default Rating;
RatingInput.js
import { useState } from 'react';
import Rating from './Rating';
import './RatingInput.css';
function RatingInput ( {name, value, onChange}) {
const [rating, setRating] = useState(value);
const handleSelect = (nextValue) => onChange(name, nextValue);
const handleMouseOut = () => setRating(value);
return (
<Rating
className = "RatingInput"
value = {rating}
onSelect = {handleSelect}
onHover = {setRating}
onMouseOut = {handleMouseOut}
/>
);
}
export default RatingInput;
ReviewForm.js
import { useState } from 'react';
import FileInput from './FileInput';
import RatingInput from './RatingInput';
import './ReviewForm.css';
function ReviewForm() {
const [values, setValues] = useState({
title: '',
rating: 0,
content: '',
imgFile: null,
});
const handleChange = (name, value) => {
setValues((prevValues) => ({
...prevValues,
[name]: value,
}));
};
const handleInputChange = (e) => {
const { name, value } = e.target;
handleChange(name, value);
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(values);
};
return (
<form className="ReviewForm" onSubmit={handleSubmit}>
<FileInput
name="imgFile"
value={values.imgFile}
onChange={handleChange}
/>
<input name="title" value={values.title} onChange={handleInputChange} />
<RatingInput
name="rating"
value={values.rating}
onChange={handleChange}
/>
<textarea
name="content"
value={values.content}
onChange={handleInputChange}
/>
<button type="submit">확인</button>
</form>
);
}
export default ReviewForm;
'react' 카테고리의 다른 글
[코드잇] 리액트로 데이터 다루기(5) (0) | 2023.09.25 |
---|---|
[코드잇] 리액트로 데이터 다루기 (4) (0) | 2023.09.24 |
[코드잇] 리액트로 데이터 다루기(2) (0) | 2023.09.04 |
[코드잇] 리액트로 데이터 다루기(1) (0) | 2023.09.02 |
[코드잇] 리액트 웹 개발 시작하기 (6) (0) | 2023.09.02 |