React 데이터 보내기

Jeris·2023년 4월 26일
0

코드잇 부트캠프 0기

목록 보기
64/107

1. 글 작성하기

  1. async await function으로 POST request를 보낼 api를 작성합니다.
// api.js
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;
}
  1. 네트워크 로딩과 오류를 처리할 state를 생성합니다.
// components/ReviewForm.js
const [isSubmitting, setIsSubmitting] = useState(false);
const [submittingError, setSubmittingError] = useState(null);
  1. submit 핸들러를 비동기화 하고 FormData() 함수로 생성한 데이터 포맷으로 POST request를 보내도록 작성합니다.
// components/ReviewForm.js
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);
};
  1. 컴포넌트에 네트워크 로딩과 오류 처리를 해줍니다.
// components/ReviewForm.js
return (
  ...
  <button type="submit" disabled={isSubmitting}>
    확인
  </button>
  {submittingError?.message && <div>{submittingError.message}</div>}
);

2. 리스폰스 데이터 반영하기

리스폰스 데이터를 컴포넌트에 바로 반영하는 방법

  1. ReviewForm 컴포넌트에 onSubmitSuccess라는 prop을 추가합니다.
// components/ReviewForm.js
function ReviewForm({ onSubmitSuccess }) ...
  1. submit 핸들러에서 onSubmitSuccess prop으로 response data를 넘겨줍니다.
// components/ReviewForm.js
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);
};
  1. App 컴포넌트에서 response data를 렌더링해주는 함수를 추가합니다.
// components/App.js
function App() {
  ...
  const handleSubmitSuccess = (review) => {
  setItems((prevItems) => [review, ...prevItems]);
  };
  1. ReviewForm 컴포넌트의 onSubmitSuccess prop으로 위의 함수를 내려줍니다.
// components/App.js
function App() {
  ...
  return (
    ...
    <ReviewForm onSubmitSuccess={handleSubmitSuccess} />
    ...
  );
}

3. 글 수정하기

  1. ReviewList 컴포넌트에 editingId state를 추가해줍니다.
// components/ReviewList.js

function ReviewList({ items, onDelete }) {
  const [editingId, setEditingId] = useState(null);
  ...
}
  1. editingId state와 item의 id가 같은 경우 ReviewForm 컴포넌트를 대신 호출합니다.
// components/ReviewList.js
function ReviewList({ items, onDelete }) {
  ...
  return (
    ...
    {items.map((item) => {
      if (item.id === editingId) {
        return (
          <li key={item.id}>
            <ReviewForm />
          </li>
        );
      }
    ...
  )
}
  1. ReviewListItem에서는 setEditingId으로 onEdit이라는 prop을 추가하고 handleEditClick이라는 함수를 만들어서 item.id로 실행해줍니다.
// components/ReviewList.js
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>
  ...
  1. 수정 버튼을 추가해줍니다.
// components/ReviewList.js
function ReviewListItem({ item, onDelete, onEdit }) {
  ...
  return (
    ...
    <button onClick={handleDeleteClick}>삭제</button>
    <button onClick={handleEditClick}>수정</button>
    ...
  );
}
  1. 취소 버튼을 추가해줍니다.
// components/ReviewForm.js
function ReviewForm({
  initialValues = INITIAL_VALUES,
  onSubmitSuccess,
  onCancel,
}) {
  const [values, setValues] = useState(initialValues);
  ...
  return (
    ...
    {onCancel && <button onClick={onCancel}>취소</button>}
    ...
  );
}
  1. ReviewList 컴포넌트에서 intialValues 값을 만들어 prop으로 내려줍니다.
// components/ReviewList.js
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>
        );
      }
    ...
  )
}
  1. ReviewList 컴포넌트에 취소 버튼 로직을 만들어줍니다.
// components/ReviewList.js
function ReviewList({ items, onDelete }) {
  ...
  const handleCancel = () => setEditingId(null);

  return (
    ...
      <ReviewForm
        initialValues={initialValues}
        onCancel={handleCancel}
      />
    ...
  )
}
  1. ReviewList 컴포넌트에 intialPreview prop을 활용해 이미지 미리보기 로직을 만들어줍니다.
// components/ReviewList.js
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>
        );
      }
    ...
  )
}
  
// components/ReviewForm.js
function ReviewForm({
  ...
  initialPreview,
  ...
}) {
  ...
  return (
    ...
    <FileInput
      name="imgFile"
      value={values.imgFile}
      initialPreview={initialPreview}
      onChange={handleChange}
    />
    ...
  )
}
  
// components/FineInput.js
function FileInput({ name, value, initialPreview, onChange }) {
  const [preview, setPreview] = useState(initialPreview);
  
  useEffect(() => {
    ...
    // cleanup function
    return () => {
      setPreview(initialPreview);
      URL.revokeObjectURL(nextPreview);
    };
  }, [value, initialPreview]);

4. 수정 API 연동하기

  1. Form이 createReview, updateReview 둘 중 하나를 불러오기 위해 ReviewForm 컴포넌트에 onSubmit prop을 생성하고 createSubmit의 이름을 onSubmit으로 바꿔줍니다.
// components/ReviewForm.js
function ReviewForm({
  initialValues = INITIAL_VALUES,
  initialPreview,
  onCancel,
  onSubmit,
  onSubmitSuccess,
}) {
  ...
  return (
    ...
    <form className="ReviewForm" onSubmit={handleSubmit}>
      ...
    </form>
    ...
  )
}
  1. App 컴포넌트에서 onsubmit prop을 createReview 값으로 ReviewForm에 내려주고 handleSubmitSuccess의 이름을 handleCreateSuccess로 바꿔줍니다.
// components/App.js
function App() {
  ...
  const handleCreateSuccess = (review) => {
    setItems((prevItems) => [review, ...prevItems]);
  };
  ...
  return (
    ...
    <ReviewForm
      onSubmit={createReview}
      onSubmitSuccess={handleCreateSuccess}
    />
    ...
  )
}
  1. updateReview PUT request API를 작성합니다.
// api.js
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;
}
  1. handleUpdateSuccess 함수를 만들고 ReviewList 컴포넌트에 onUpdate, onUpdateSuccess prop을 내려줍니다.
// components/App.js
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}
    />
    ...
  )
}
  1. 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. 글 삭제하기

  1. deleteReview delete request API를 작성합니다.
// 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;
}
  1. App 컴포넌트에서 handleDelete 함수를 비동기 함수로 바꿔줍니다.
// components/App.js
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의 규칙

  1. 리액트 Hook은 반드시 함수형 컴포넌트나 커스텀 Hook 함수 안에서 실행되어야 합니다.
  2. 리액트 Hook은 반드시 함수의 최상위에서 실행해야 합니다. 즉, 중첩된 함수, 반복문 또는 조건문 안에서 호출하면 안됩니다.
  • Hook을 호출할 때는 항상 동일한 순서로 Hook을 호출해야 하기 때문입니다.
  1. 커스텀 Hook을 포함해서, 이름은 항상 "use"로 시작해야 합니다.
  2. Hook은 state나 side effect를 관리하기 위해 사용해야 합니다.

8. 나만의 Hook으로 코드 정리하기

네트워크 로딩/오류를 처리하는 커스텀 Hook 만들기

// hooks/useAsync.js
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()이란?

  • useCallback()은 콜백 함수를 캐싱하고 재사용하기 위해 사용되는 React Hook입니다.
  • useCallback()dependency array가 변경될 때만 콜백 함수를 생성합니다.
  • useCallback()은 주로 자식 컴포넌트에 props로 전달되는 콜백 함수를 최적화하는데 사용됩니다.
  • useCallback()은 다음과 같은 형태로 사용됩니다.
    const memoizedCallback = useCallback(
      () => {
        // contents
      },
      [/* dependency array */],
    );


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

profile
job's done

0개의 댓글