1. 글 작성하기
- async await function으로 POST request를 보낼 api를 작성합니다.
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;
}
- 네트워크 로딩과 오류를 처리할 state를 생성합니다.
const [isSubmitting, setIsSubmitting] = useState(false);
const [submittingError, setSubmittingError] = useState(null);
- submit 핸들러를 비동기화 하고 FormData() 함수로 생성한 데이터 포맷으로 POST request를 보내도록 작성합니다.
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_VALUE);
};
- 컴포넌트에 네트워크 로딩과 오류 처리를 해줍니다.
return (
...
<button type="submit" disabled={isSubmitting}>
확인
</button>
{submittingError?.message && <div>{submittingError.message}</div>}
);
2. 리스폰스 데이터 반영하기
리스폰스 데이터를 컴포넌트에 바로 반영하는 방법
- ReviewForm 컴포넌트에
onSubmitSuccess
라는 prop을 추가합니다.
function ReviewForm({ onSubmitSuccess }) ...
- submit 핸들러에서
onSubmitSuccess
prop으로 response data를 넘겨줍니다.
const handleSubmit = async (e) => {
...
let result;
try {
setSubmittingError(null);
setIsSubmitting(true);
result = await createReview(formData);
} catch (error) {
setSubmittingError(error);
return;
} finally {
setIsSubmitting(false);
}
const { review } = result;
setValues(INITIAL_VALUES);
onSubmitSuccess(review);
};
- App 컴포넌트에서 response data를 렌더링해주는 함수를 추가합니다.
function App() {
...
const handleSubmitSuccess = (review) => {
setItems((prevItems) => [review, ...prevItems]);
};
- ReviewForm 컴포넌트의
onSubmitSuccess
prop으로 위의 함수를 내려줍니다.
function App() {
...
return (
...
<ReviewForm onSubmitSuccess={handleSubmitSuccess} />
...
);
}
3. 글 수정하기
- ReviewList 컴포넌트에
editingId
state를 추가해줍니다.
function ReviewList({ items, onDelete }) {
const [editingId, setEditingId] = useState(null);
...
}
editingId
state와 item의 id가 같은 경우 ReviewForm 컴포넌트를 대신 호출합니다.
function ReviewList({ items, onDelete }) {
...
return (
...
{items.map((item) => {
if (item.id === editingId) {
return (
<li key={item.id}>
<ReviewForm />
</li>
);
}
...
)
}
- ReviewListItem에서는
setEditingId
으로 onEdit이라는 prop을 추가하고 handleEditClick
이라는 함수를 만들어서 item.id로 실행해줍니다.
function ReviewListItem({ item, onDelete, onEdit }) {
...
const handleEditClick = () => {
onEdit(item.id);
}
...
}
...
function ReviewList({ items, onDelete }) {
...
return (
<li key={item.id}>
<ReviewListItem
item={item}
onDelete={onDelete}
onEdit={setEditingId}
/>
</li>
...
- 수정 버튼을 추가해줍니다.
function ReviewListItem({ item, onDelete, onEdit }) {
...
return (
...
<button onClick={handleDeleteClick}>삭제</button>
<button onClick={handleEditClick}>수정</button>
...
);
}
- 취소 버튼을 추가해줍니다.
function ReviewForm({
initialValues = INITIAL_VALUES,
onSubmitSuccess,
onCancel,
}) {
const [values, setValues] = useState(initialValues);
...
return (
...
{onCancel && <button onClick={onCancel}>취소</button>}
...
);
}
- ReviewList 컴포넌트에서
intialValues
값을 만들어 prop으로 내려줍니다.
function ReviewList({ items, onDelete }) {
...
return (
...
{items.map((item) => {
if (item.id === editingId) {
const { title, rating, content } = item;
const initialValues = { title, rating, content };
return (
<li key={item.id}>
<ReviewForm initialValues={initialValues} />
</li>
);
}
...
)
}
- ReviewList 컴포넌트에 취소 버튼 로직을 만들어줍니다.
function ReviewList({ items, onDelete }) {
...
const handleCancel = () => setEditingId(null);
return (
...
<ReviewForm
initialValues={initialValues}
onCancel={handleCancel}
/>
...
)
}
- ReviewList 컴포넌트에
intialPreview
prop을 활용해 이미지 미리보기 로직을 만들어줍니다.
function ReviewList({ items, onDelete }) {
...
return (
...
{items.map((item) => {
if (item.id === editingId) {
const { imgUrl, title, rating, content } = item;
const initialValues = { title, rating, content };
return (
<li key={item.id}>
<ReviewForm
initialValues={initialValues}
initialPreview={imgUrl}
onCancel={handleCancel}
/>
</li>
);
}
...
)
}
function ReviewForm({
...
initialPreview,
...
}) {
...
return (
...
<FileInput
name="imgFile"
value={values.imgFile}
initialPreview={initialPreview}
onChange={handleChange}
/>
...
)
}
function FileInput({ name, value, initialPreview, onChange }) {
const [preview, setPreview] = useState(initialPreview);
useEffect(() => {
...
return () => {
setPreview(initialPreview);
URL.revokeObjectURL(nextPreview);
};
}, [value, initialPreview]);
4. 수정 API 연동하기
- Form이 createReview, updateReview 둘 중 하나를 불러오기 위해 ReviewForm 컴포넌트에
onSubmit
prop을 생성하고 createSubmit
의 이름을 onSubmit
으로 바꿔줍니다.
function ReviewForm({
initialValues = INITIAL_VALUES,
initialPreview,
onCancel,
onSubmit,
onSubmitSuccess,
}) {
...
return (
...
<form className="ReviewForm" onSubmit={handleSubmit}>
...
</form>
...
)
}
- App 컴포넌트에서
onsubmit
prop을 createReview
값으로 ReviewForm에 내려주고 handleSubmitSuccess
의 이름을 handleCreateSuccess
로 바꿔줍니다.
function App() {
...
const handleCreateSuccess = (review) => {
setItems((prevItems) => [review, ...prevItems]);
};
...
return (
...
<ReviewForm
onSubmit={createReview}
onSubmitSuccess={handleCreateSuccess}
/>
...
)
}
updateReview
PUT request 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;
}
handleUpdateSuccess
함수를 만들고 ReviewList 컴포넌트에 onUpdate
, onUpdateSuccess
prop을 내려줍니다.
function App() {
...
const handleUpdateSuccess = (review) => {
setItems((prevItems) => {
const splitIdx = prevItems.findIndex((item) => item.id === review.id);
return [
...prevItems.slice(0, splitIdx),
review,
...prevItems.slice(splitIdx + 1),
];
});
};
...
return (
...
<ReviewList
items={sortedItems}
onDelete={handleDelete}
onUpdate={updateReview}
onUpdateSuccess={handleUpdateSuccess}
/>
...
)
}
- ReviewList 컴포넌트에서
onUpdate
, onUpdateSuccess
prop 처리를 해줍니다.
function ReviewList({ items, onDelete, onUpdate, onUpdateSuccess }) {
...
return (
...
const handleSubmit = (formData) => onUpdate(id, formData);
const handleSubmitSuccess = (review) => {
onUpdateSuccess(review);
setEditingId(null);
};
return (
<li key={item.id}>
<ReviewForm
initialValues={initialValues}
initialPreview={imgUrl}
onCancel={handleCancel}
onSubmit={handleSubmit}
onSubmitSuccess={handleSubmitSuccess}
/>
</li>
...
);
);
}
5. 글 삭제하기
deleteReview
delete request API를 작성합니다.
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
함수를 비동기 함수로 바꿔줍니다.
function App() {
...
const handleDelete = async (id) => {
const result = await deleteReview(id);
if (!result) return;
setItems((prevItems) => prevItems.filter((item) => item.id !== id));
};
...
}
6. 리액트 Hook
프로그래밍에서 Hook
- 내가 작성한 코드를 다른 프로그램에 연결해서 그 값이나 기능을 사용하는 것
useState
- useSatet는 리액트의 State라는 기능에 연결해서 변수처럼 값을 사용하는 Hook입니다.
- useState로 생성한 state는 컴포넌트 안에 있는 값이 아니라 리액트가 따로 관리하는 값입니다.
useEffect
- useEffect는 내 콜백 함수를 리액트에 연결하고, 렌더링 이후에 함수를 실행하는 Hook입니다.
useRef
- useRef는 리액트가 관리하는 Ref 객체에 연결해서 current같은 값을 사용할 수 있게 해주는 Hook입니다.
7. 리액트 Hook의 규칙
- 리액트 Hook은 반드시 함수형 컴포넌트나 커스텀 Hook 함수 안에서 실행되어야 합니다.
- 리액트 Hook은 반드시 함수의 최상위에서 실행해야 합니다. 즉, 중첩된 함수, 반복문 또는 조건문 안에서 호출하면 안됩니다.
- Hook을 호출할 때는 항상 동일한 순서로 Hook을 호출해야 하기 때문입니다.
- 커스텀 Hook을 포함해서, 이름은 항상 "use"로 시작해야 합니다.
- Hook은 state나 side effect를 관리하기 위해 사용해야 합니다.
8. 나만의 Hook으로 코드 정리하기
네트워크 로딩/오류를 처리하는 커스텀 Hook 만들기
import { useState } from "react";
function useAsync(asyncFunction) {
const [pending, setPending] = useState(false);
const [error, setError] = useState(null);
const wrappedFunction = async (...args) => {
try {
setError(null);
setPending(true);
return await asyncFunction(...args);
} catch (error) {
setError(error);
return;
} finally {
setPending(false);
}
};
return [pending, error, wrappedFunction];
}
export default useAsync;
9. useCallback
useCallback()이란?
10. 빠짐없는 디펜던시(exhaustive-deps)
exhaustive-deps 규칙
- exhaustive-deps는 의존성 배열(dependency array)을 검사하여 누락된
dependency
가 있는지를 검사하는 규칙입니다.
- React Hook이 side effect를 처리할 때, 해당 Hook의
dependency array
에는 이 side effect에서 참조하는 모든 값이 포함되어야 합니다. 그렇지 않으면 불필요한 리렌더링이 발생할 수 있습니다.
- exhaustive-deps 규칙은 이러한 누락된
dependency
를 방지하기 위해, dependency array
에 누락된 값이 있는 경우 경고 메시지를 출력합니다. 이를 통해 개발자가 누락된 dependency
를 추가하거나, 불필요한 dependency
를 제거하여 최적화할 수 있습니다.
dependency
에 포함되어야 할 값이 누락된 경우, ESLint나 TSLint 등의 정적 분석 도구를 사용하여 경고 메시지를 확인할 수 있습니다.
- 컴포넌트 안에서 만든 함수를
dependency array
에 사용할 때는 useCallback
hook으로 매번 함수를 새로 생성하는 걸 방지할 수 있습니다.
- 컴포넌트 안에서 만든 함수에서 prop이나 state 값을 사용할 때는 가능하면 파라미터로 넘겨서 사용하는 것이 좋습니다.
Feedback
- 리액트 튜토리얼로 앱을 만들었으니 실제 프로젝트를 구상하여 만들어보자.
- Reference에 써놓은 문서들을 자세히 읽어보자.
Reference