export async function createReview(formData) {
const response = await fetch(`${BASE_URL}/film-reviews`, {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error("리뷰를 생성하는데 실패했습니다.");
}
const body = await response.json();
return body;
}
api에 POST를 보내고
import { useState } from "react";
import { createReview } from "../api";
import FileInput from "./FileInput";
import RatingInput from "./RatingInput";
import "./ReviewForm.css";
const INITIAL_VALUES = {
title: "",
rating: 0,
content: "",
imgFile: null,
};
function ReviewForm() {
const [values, setValues] = useState(INITIAL_VALUES);
const [isSubmitting, setIsSubmitting] = useState(false);
const [submittingError, setSubmittingError] = useState(null);
const handleChange = (name, value) => {
setValues((prevValues) => ({
...prevValues,
[name]: value,
}));
};
const handleInputChange = (e) => {
const { name, value } = e.target;
handleChange(name, value);
};
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append("title", values.title);
formData.append("rating", values.rating);
formData.append("content", values.content);
formData.append("imgFile", values.imgFile);
try {
setSubmittingError(null);
setIsSubmitting(true);
await createReview(formData);
} catch (error) {
setSubmittingError(error);
return;
} finally {
setIsSubmitting(false);
}
setValues(INITIAL_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 disabled={isSubmitting} type="submit">
확인
</button>
{submittingError && <div>{submittingError.message}</div>}
</form>
);
}
export default ReviewForm;
handleSubmit 함수를 통해 내용을 추가한다. 마지막에는 입력창을 비워줘야 하니까 초기 상태(INITIAL_VALUES)로 비워준다.
리퀘스트 도중에 버튼을 누를 수 없게 비활성화도 해준다.
새로고침 없이 화면에 리스폰스 데이터를 반영하는 법
현재 화면에 나오는 리뷰 목록은 App 컴포넌트의 items state에서 관리되고 있다.
리뷰 생성하고 받은 리퀘스트 데이터를 items state에 추가해주면 별도로 리퀘스트를 하지 않아도 리뷰 목록을 업데이트할 수 있다.
ReviewList.js
import { useState } from 'react';
import Rating from './Rating';
import ReviewForm from './ReviewForm';
import './ReviewList.css';
function formatDate(value) {
const date = new Date(value);
return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}`;
}
function ReviewListItem({ item, onDelete, onEdit }) {
const handleDeleteClick = () => {
onDelete(item.id);
};
const handleEditClick = () => {
onEdit(item.id);
};
return (
<div className="ReviewListItem">
<img className="ReviewListItem-img" src={item.imgUrl} alt={item.title} />
<div>
<h1>{item.title}</h1>
<Rating value={item.rating} />
<p>{formatDate(item.createdAt)}</p>
<p>{item.content}</p>
<button onClick={handleEditClick}>수정</button>
<button onClick={handleDeleteClick}>삭제</button>
</div>
</div>
);
}
function ReviewList({ items, onDelete }) {
const [editingId, setEditingId] = useState(null);
const handleCancel = () => setEditingId(null);
return (
<ul>
{items.map((item) => {
if (item.id === editingId) {
const { imgUrl, title, rating, content } = item;
const initialValues = { title, rating, content, imgFile: null };
return (
<li key={item.id}>
<ReviewForm
initialValues={initialValues}
initialPreview={imgUrl}
onCancel={handleCancel}
/>
</li>
);
}
return (
<li key={item.id}>
<ReviewListItem
item={item}
onDelete={onDelete}
onEdit={setEditingId}
/>
</li>
);
})}
</ul>
);
}
export default ReviewList;
editingId state를 추가해서 수정 버튼을 누르면 입력폼이 나타나도록 한다.
수정 버튼을 눌렀을 때 내용이 나오고 취소버튼 생성, 이미지도 보여줄 수 있게끔 initialPreview, imgURL
수정할 때는 다른 api를 활용한다.
export async function updateReview(id, formData) {
const response = await fetch(`${BASE_URL}/film-reviews/${id}`, {
method: 'PUT',
body: formData,
});
if (!response.ok) {
throw new Error('리뷰를 수정하는데 실패했습니다.');
}
const body = await response.json();
return body;
}
리퀘스트 함수를 구현하고 연결해주면된다.
api.js
export async function deleteReview(id) {
const response = await fetch(`${BASE_URL}/film-reviews/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('리뷰를 삭제하는데 실패했습니다.');
}
const body = await response.json();
return body;
}
App 컴포넌트의 handleDelete 함수를 비동기 함수로 만든다.
비동기로 state를 참조하는거니까 세터 함수는 콜백으로 사용
const handleDelete = async (id) => {
const result = await deleteReview(id);
if (!result) return;
setItems((prevItems) => items.filter((item) => item.id !== id));
};
useState, useEffect, useRef 등 use로 시작하는 것들을 Hook이라고 부른다.
리액트가 제공하는 기능에 연결해서 그 값이나 기능을 사용하는 함수
리액트는 훅이 실행된 순서대로 연결되기 때문에 반복문이나 조건문 안에서 사용할 수 없다.
훅들을 파일 하나에 모아서 정리할 수도 있다.
맨앞에 use 라는 단어를 붙여서 다른 개발자들이 Hook이라는 걸 알 수 있게 해줘야 한다.
useAsync.js
import { useState } from 'react';
function useAsync(asyncFunction) {
const [pending, setPending] = useState(false);
const [error, setError] = useState(null);
const wrappedFunction = async (...args) => {
setPending(true);
setError(null);
try {
return await asyncFunction(...args);
} catch (error) {
setError(error);
} finally {
setPending(false);
}
};
return [pending, error, wrappedFunction];
}
export default useAsync;
...
재 렌더링을 방지하기 위해 사용한다.
useCallback(() => {
return value;
}. [item])
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
const addCount = () => {
setCount(c => c + 1);
console.log(`num: ${num}`);
}
const addNum = () => setNum(n => n + 1);
useEffect(() => {
console.log('timer start');
const timerId = setInterval(() => {
addCount();
}, 1000);
return () => {
clearInterval(timerId);
console.log('timer end');
};
}, []);
return (
<div>
<button onClick={addCount}>count: {count}</button>
<button onClick={addNum}>num: {num}</button>
</div>
);
}
export default App;
위 코드에서 num의 state가 바뀌더라도 콘솔 숫자에서는 숫자가 바뀌지 않고 0만 출력된다는 문제가 있다.
num state 값을 잘못 참조하고 있기 때문이다. (과거의 num state 값을 참조하고 있음)
이런 문제점을 경고해주는 규칙이 react-hooks/exhaustive-deps
라는 규칙이다.
리액트에서는 Prop이나 State와 관련된 값은 되도록이면 빠짐없이 디펜던시에 추가해서 항상 최신 값으로 useEffect 나 useCallback 을 사용하도록 권장하고 있다.
addCount 함수를 디펜던시 리스트로 추가하면
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
const addCount = () => {
setCount(c => c + 1);
console.log(`num: ${num}`);
}
const addNum = () => setNum(n => n + 1);
useEffect(() => {
console.log('timer start');
const timerId = setInterval(() => {
addCount();
}, 1000);
return () => {
clearInterval(timerId);
console.log('timer end');
};
}, []);
return (
<div>
<button onClick={addCount}>count: {count}</button>
<button onClick={addNum}>num: {num}</button>
</div>
);
}
export default App;
이렇게 된다.
하지만 여기서 문제가 있는데
count가 바뀔 때마다 타이머를 새로 시작하고 종료하는 것을 반복한다는 것이다.
import { useCallback, useEffect, useState } from "react";
function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
const addCount = useCallback(() => {
setCount((c) => c + 1);
console.log(`num: ${num}`);
}, [num]);
const addNum = () => setNum((n) => n + 1);
useEffect(() => {
console.log('timer start');
const timerId = setInterval(() => {
addCount();
}, 1000);
return () => {
clearInterval(timerId);
console.log('timer end');
};
}, [addCount]);
return (
<div>
<button onClick={addCount}>count: {count}</button>
<button onClick={addNum}>num: {num}</button>
</div>
);
}
export default App;
디펜던시 리스트에 추가한 함수가 매번 바뀌는 문제를 해결하려면 함수를 useCallback으로 감싸주면된다.
useCallback을 사용하면 함수를 매번 사용하는 것이 아니라 함수를 기억해두고 디펜던시 리스트 값이 바뀔때만 함수를 새로 만들어준다.
이런 식으로 컴포넌트 안에서 만든 함수를 디펜던시 리스트에 사용할 때는 useCallback 훅으로 매번 함수를 새로 생성하는 걸 막을 수 있다.