본문 바로가기

코드스테이츠 SEB FE 41기/Section 별 내용 정리

section4/Unit4/[React] 심화(11/29)

반응형

과제 : React Hooks 적용하기

1. json-server 설치

가짜 REST API 서버를 이용하여 json 데이터를 직접 받아와 실제 블로그처럼 되도록 코드를 작성한다.
먼저 가짜 서버를 설치한다.

// json-server는 라이브러리로, json 파일을 이용하여 REST API 서버를 구축해주는 라이브러리이다.
// 터미널을 열어 npm을 이용해 전역 설치를 해준다. react 앱 파일 내에 설치하면 제대로 작동하지 않을 수 있다.
$ npm i -g json-server
// 그 다음에 앱 내에 존재하고 있는 data 폴더로 이동한 뒤, 다음 명령어를 입력해 준다.
// —port 3001이라는 옵션을 뒤에 붙여주지 않으면 json-server는 저절로 3000번 포트를 점유하고 
// 서버를 열게 되므로, 꼭 포트 번호를 뒤에 붙여 정확히 어느 포트에 서버를 여는 것인지 인지해야 한다.
$ cd data 
$ json-server --watch data.json --port 3001

localhost:3001 로 진입하자 server가 잘 열린 것을 확인할 수 있다.

[그림] 포스트맨을 통해 GET 요청을 보내보면 정상적으로 응답하고 있다.

포스트맨의 Workspaces에서 HTTP request를 새로이 생성하여, json-server가 만들어준 API에 GET 요청을 보내보면, json 파일에 들어있는 내용 그대로 응답을 하고 있음을 알 수 있다.

 

2. App 루트 컴포넌트

App.js 에서 Route로 설정한 컴포넌트들을 lazy 로 바꾸고 Routes를 Suspense로 감싼다.

import Home './Home'
import CreateBlog './blogComponent/CreateBlog'
import BlogDetail './blogComponent/BlogDetail'
import NotFound './component/NotFound'

// static으로 import되어 있는 부분들을 다 지우고

import React, { Suspense, lazy } from 'react';

// lazy를 사용한 코드를 추가한뒤,
const Home = React.lazy( () => import('./Home'))
const CreateBlog = React.lazy( () => import('./blogComponent/CreateBlog'))
const  BlogDetails = React.lazy( () => import('./blogComponent/BlogDetail'))
const NotFound = React.lazy( () => import('./component/NotFound'))

// Suspense로 감싸준다.
 <Suspense fallback={<div>로딩...</div>}>
   <Routes>
       // ...생략
   </Routes>
 </Suspense>

 

3. BlogDeatil 컴포넌트

1) 작성된 내용 클릭시 세부내용 화면 띄우기

리액트에서 라우터 사용 시 파라미터 정보를 가져와 활용하고 싶다면 useParams라는 훅을 사용하면 된다. 라우터를 사용하고 있다는 가정 하에 useParams 사용 방법에 대해 알아보도록 하겠다.

참고로 파라미터가 아닌 현재 페이지의 Pathname을 가져오려면 useLocation()을 사용해야 한다.

...URL/user/1
user는 pathname, 1은 parameter

useParams를 사용하기 위해서는 라우터 설치가 필수다. 

npm install react-router-dom

각 블로그 글마다 각자의 id가 있다. 개별 id를 useParams()를 통해 받아와, fetch 요청할 때 사용할 수 있도록 해준다.

// 동적 매개변수 부분을 가져온다.
const {id} = useParams();
// console.log(id);
const request = { 
   method : "get" , 
   headers : {'Content-Type': 'application/json'},
}
...
fetch(`http://localhost:3001/blogs/${id}`, request)

 

2) 삭제 / 하트

- 삭제

const navigate = useNavigate();
...
const handleDeleteClick = () => {
    /* delete 버튼을 누르면 다시 home으로 리다이렉트 되어야 합니다. */
    /* useNavigate()를 이용하여 로직을 작성해주세요. */
    fetch(`http://localhost:3001/blogs/${id}`, {
      method: "DELETE",
      headers: { 'Content-Type': 'application/json' },
    })
      .then(() => {
        // 첫 화면으로 자동으로 이동
        navigate('/');
        // 정상 응답을 받으면 새로고침 되도록(json-server 를 사용하기 때문)
        window.location.reload();
      })
      .catch(err => console.log(err));
  }

- 하트(홈화면에서는 새로고침해야 적용된 것 확인할 수 있음)

const handleLikeClick = () => {
    /* 하트를 누르면 home에서 새로고침을 했을 때 숫자가 올라가야 합니다. */
    /* isLike와 blog.likes를 이용하여 handleLikeClick의 로직을 작성해주세요. */
    // isLike false 인경우 true 로 바뀐다.
    // navigate를 통해 리다이렉트 된 후 isLike의 변한 값이 반영된다.
    setIsLike(!isLike);
    let likeUpdate = blogs.likes;

    // isLike state가 false 이고 0보다 큰경우 하트 수 감소하고 0이면 그대로. true면 증가  
    isLike === false ?
      (likeUpdate > 0 ? likeUpdate = blogs.likes - 1 : likeUpdate = blogs.likes)
      : likeUpdate = blogs.likes + 1

    // 기존 데이터를 덮어씌우기 때문에 데이터 모든 프로퍼티를 다시 보내줘야 한다.
    // likes만 달라진다.
    const patchData = {
      'id': blogs.id,
      "title": blogs.title,
      "body": blogs.body,
      "author": blogs.author,
      "likes": likeUpdate,
    }
    // console.log(likeUpdate)
    fetch(`http://localhost:3001/blogs/${id}`, {
      method: "PATCH",
      body: JSON.stringify(patchData),
      headers: {
        // 지금 보내는 데이터가 json 형태이다.
        'Content-Type': 'application/json'
      },
    })
      .then(() => {
        navigate(`/blogs/${blogs.id}`)
      })
      .catch(err => console.log(err))
  }

 

4. CreateBlog 컴포넌트

import Footer from "../component/Footer";
...
const navigate = useNavigate();

  const handleSubmit = (e) => {
    /* 등록 버튼을 누르면 게시물이 등록이 되며 home으로 리다이렉트 되어야 합니다. */
    /* 작성한 내용과 useNavigate를 이용하여 작성해보세요. */
    e.preventDefault();
    // 데이터 속 각 요소는 객체이므로, 똑같이 작성한 내용 객체로 묶어주기
    const bodys = { title, body, author, likes: 0 };
    const request = {
      method: "POST",
      body: JSON.stringify(bodys),
      headers: { 'Content-Type': 'application/json' }
    }
    fetch('http://localhost:3001/blogs', request)
      .then(() => {
        // 홈으로 리다이렉트
        navigate('/')
        // 정상 응답을 받으면 새로고침(json-server 를 사용하기 때문)
        window.location.reload();
      })
      .catch(err => console.log(err));
    console.log(e.type);
  }
  ...
// <div> 태그 안에 Footer 컴포넌트를 넣어준다.
<div className="create">
   ...
   <Footer />
</div>

 

5. useFetch 컴포넌트

import { useState, useEffect } from 'react';

export const useFetch = (url) => {
  const [blogs, setBlogs] = useState(null);
  const [isPending, setIsPending] = useState(true);
  const [error, setError] = useState(null); 
  /* useState를 이용하여 data, isPending, error를 정의하세요. */

  /* useFetch 안의 중심 로직을 작성해주세요. */
  useEffect(() => {
    setTimeout(() => {
      fetch(url)
      .then(res => {
        if (!res.ok) {
          throw Error('could not fetch the data for that resource');
        } 
        return res.json();
      })
      .then(data => {
        setIsPending(false);
        setBlogs(data);
        setError(null);
      })
      .catch(err => {
        setIsPending(false);
        setError(err.message);
      })
    }, 500);
  }, [])
  return [blogs , isPending , error]
} /* return 문을 작성해주세요. */
 
export default useFetch;

데이터가 필요한 곳에서 구조분해할당을 이용해 데이터를 꺼내 쓸 수 있다.

App.js

const [blogs, isPending, error] = useFetch('http://localhost:3001/blogs');

BlogDetail.js

const BlogDetails = () => {
  const [isLike, setIsLike] = useState(true);
  const navigate = useNavigate();

  /* 현재는 개별 블로그 내용으로 진입해도 내용이 보이지 않습니다. */
  /* id를 이용하여 개별 블로그의 내용이 보일 수 있게 해봅시다. */
  const { id } = useParams();

  const [blogs, isPending, error] = useFetch(`http://localhost:3001/blogs/${id}`);

  const handleDeleteClick = () => {
    /* delete 버튼을 누르면 다시 home으로 리다이렉트 되어야 합니다. */
    /* useNavigate()를 이용하여 로직을 작성해주세요. */
    fetch(`http://localhost:3001/blogs/${id}`, {
      method: "DELETE",
      headers: { 'Content-Type': 'application/json' },
    })
      .then(() => {
        // 첫 화면으로 자동으로 이동
        navigate('/');
        // 정상 응답을 받으면 새로고침 되도록
        window.location.reload();
      })
      .catch(err => console.log(err));
  }

  const handleLikeClick = () => {
    /* 하트를 누르면 home에서 새로고침을 했을 때 숫자가 올라가야 합니다. */
    /* isLike와 blog.likes를 이용하여 handleLikeClick의 로직을 작성해주세요. */
    // isLike false 인경우 true 로 바뀐다.
    // navigate를 통해 리다이렉트 된 후 isLike의 변한 값이 반영된다.
    setIsLike(!isLike);
    let likeUpdate = blogs.likes;

    // isLike state가 false 이고 0보다 큰경우 하트 수 감소하고 0이면 그대로. true면 증가  
    isLike === false ?
      (likeUpdate > 0 ? likeUpdate = blogs.likes - 1 : likeUpdate = blogs.likes)
      : likeUpdate = blogs.likes + 1

    // 기존 데이터를 덮어씌우기 때문에 데이터 모든 프로퍼티를 다시 보내줘야 한다.
    // likes만 달라진다.
    const putData = {
      'id': blogs.id,
      "title": blogs.title,
      "body": blogs.body,
      "author": blogs.author,
      "likes": likeUpdate,
    }
    // console.log(likeUpdate)
    fetch(`http://localhost:3001/blogs/${id}`, {
      method: "PUT",
      body: JSON.stringify(putData),
      headers: {
        'Content-Type': 'application/json'
      },
    })
      .then(() => {
        navigate(`/blogs/${blogs.id}`)
      })
      .catch(err => console.log(err))
  }


  return (
    <div className="blog-details">
      {isPending && <div>Loading...</div>}
      {error && <div>{error}</div>}
      {blogs && (
        <article>
          <h2>{blogs.title}</h2>
          <p>Written by {blogs.author}</p>
          <div>{blogs.body}</div>
          <button onClick={handleLikeClick}>
            {/* isLike에 의해 조건부 렌더링으로 빨간 하트(❤️)와 하얀 하트(🤍)가 번갈아 보여야 합니다. */}
            {!isLike ? '❤️' : '🤍'}
          </button>
          <button onClick={handleDeleteClick}>delete</button>
        </article>
      )}
    </div>
  );
}

 

 

반응형