[Section 4 마무리 - 솔로 프로젝트] CRUD 블로그 만들기

young·2022년 8월 17일
1

7/21~8/18 Section 4 TIL

목록 보기
21/22
post-thumbnail

📢 목표

  • CRUD 애플리케이션 구현하기

나는 섹션3 솔로 프로젝트에서 뼈대를 만들어놨던 미니 블로그를 계속 만들기로 했다.


📌 개발

json-server

기존에 redux를 이용해서 store을 생성하고 dummy data를 state로 관리하고 있었는데,
이번 섹션에서 배웠던 json-server을 활용해보고자 하여 json-server를 설치하고 data를 json 형식으로 변경했다.

$ npm i -g json-server //최상위 경로에 전역 설치
$ json-server --watch data.json --port 3001 //data.json 파일이 있는 위치에서 json 서버 열기

서버를 열면 API가 자동 생성되어 http://localhost:3001 에서 data를 확인할 수 있다.



Custom hook (useFetch) GET 요청 보내기

기존에 글 데이터와 댓글 데이터를 나누어 endpoiont를 만들었어서 각각에 대한 fetch 요청이 필요했다.
(현재는 데이터를 화면에 뿌리는 데에 글, 댓글을 하나로 묶는 게 편할 것 같아서 합쳐 놓은 상태다.)
따라서 data를 불러오는 함수를 자주 사용하게 될 것 같아서 해당 부분을 custom hook으로 만들어놨다.

import { useState, useEffect } from 'react';

const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [isPending, setIsPending] = useState(true);
  const [error, setError] = useState(null);
  
  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);
        setData(data);
        setError(null);
      })
      .catch(err => {
        setIsPending(false);
        setError(err.message);
      })
    }, 1000);
  }, [url])

  return {data, isPending, error}
}


export default useFetch;

return 값으로 data, pending 여부, error를 반환한다.

App 컴포넌트에서 위 요청을 실행하고, 반환되는 data를 하위 컴포넌트에 props로 전달해서 사용했다.



React.lazy() / loading Indicatior / SVG

App 컴포넌트에서 React.lazy() 함수로 dynamic import를 사용해 하위 컴포넌트를 렌더링했다.

lazy 컴포넌트들을 Suspense 컴포넌트로 감싸주고, Suspense 컴포넌트의 fallback props으로 컴포넌트가 렌더링 완료되는 동안 보여질 로딩 컴포넌트를 설정했다.

로딩 컴포넌트는 svg 파일을 사용하는데, svg 파일을 사용하는 방법을 찾아보니 아래와같이 컴포넌트화 하여 로딩 컴포넌트에 불러오는 방법이 있었다.

import { ReactComponent as LoadingSvg } from "../../loading.svg";

const Loading = () => {
    return (
        <section>
            <h1>Loading...</h1>
            <LoadingSvg />
        </section>
    )
}


Create :: 글 작성 (POST)

MainPage에서 글 작성 버튼을 누르면 PostWritePage로 라우팅되고,
PostWritePage 컴포넌트에서 글 작성은 POST 요청을 통해 이루어진다.

POST 요청의 옵션 헤더 headers: { "Content-Type": "application/json" }를 설정하고,
title,content과 그외 다른 초기값들을 newPost 변수에 담아준 뒤 요청 바디에 이를 json 형식으로 변경해서 보내준다.
글 작성이 끝나면 endpoint /로 이동하고, 화면을 새로고침하여 json-server의 데이터를 새로 받아오도록 했다.

const onSubmitInput = (event) => {
  event.preventDefault();
  const newPost = {
    title: title,
    author: "멍멍",
    content: content,
    date: new Date().toLocaleDateString(),
    img: "",
    comments: [],
  };
  const options = {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(newPost),
  };
  fetch(`http://localhost:3001/post`, options)
    .then((res) => res.text())
    .catch((error) => console.log("error", error));
  navigate("/");
  window.location.reload();
}


Update :: 댓글 작성 (PATCH)

PostViewPage 컴포넌트에서 댓글 작성은 PATCH 요청을 통해 이루어진다.

글 작성보다 댓글 작성이 까다로웠다.
그 이유는, 내가 만든 json 데이터는 아래와 같았는데,

 {
      "id": 3,
      "author": "멍멍",
      "title": "7월 지그재그 장바구니 위시 리스트",
      "content": "더우니까 옷 사고 싶네~~ 나갈 데도 없는데",
      "date": "2022. 6. 22",
      "img": "https://cdn.pixabay.com/photo/2016/04/08/18/46/shopping-mall-1316787__480.jpg",
      "comments": [
        {
          "id": 1,
          "content": "소통해요~"
        }
      ]
 },

글 data 안에 하위로 댓글 data가 들어가있는 형식이었다.
전체 데이터를 뿌려주는 데에는 글과 댓글을 한꺼번에 처리하는 게 용이했지만,
댓글 data를 create, update하는 데에는 조금 까다로웠다.

comments : [ ... ] 이런식으로 요청 바디에 데이터를 보내면 comments 안에 id값이 자동 생성되지 않고, 상위(=글)의 id값이 자동 생성되는 것이었다 (!)

따라서 새로운 댓글을 작성할 때 content 뿐만 아니라 id값도 내가 직접 생성해서 보내주어야 했다.

글 부분은 그대로 두고, comments 배열 부분만 update되는 것이기 때문에 일부분만 수정하는 PATCH method를 사용하게 되었다.

먼저 id 값을 생성하는 방법은 아래와 같다.

const [maxNum, setMaxNum] = useState();

useEffect(() => {
  if (selectedComments.length === 0) { //selectedComments = 현재 글의 댓글 data. 즉 comments 배열
    setMaxNum(1)
  }
  else {
    setMaxNum(Math.max(
      ...selectedComments.map((v) => {
        return v.id;
      })
    ) + 1)
  }
}, []);

해당 컴포넌트가 mount되면 id값 생성 함수가 작동한다.
댓글 배열이 빈 배열이라면 id값은 1부터 시작하고,
그 외에는 현재 댓글 배열에 있는 최대 id값 + 1부터 시작한다.

이렇게 생성한 id값과, input에서 얻은 value를 요청 바디에 담아주고, data의 일부분만 변경하는 PATCH 요청을 보낸다.

const onSubmitHandler = (event) => {
    event.preventDefault();
    const options = {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        comments: [...selectedComments, { content: value, id: maxNum }],
      }),
    };
    fetch(`http://localhost:3001/post/${postId}`, options)
      .then((res) => res.text())
      .then((result) => console.log(result))
      .catch((error) => console.log("error", error));
    window.location.reload();
  };

요청이 끝나면 화면을 새로고침하여 update된 data를 화면에 렌더링하도록 했다.



Update :: 글 수정 (PATCH)

PostViewPage 컴포넌트에서 글 수정하기 버튼을 누르면
-> isModifyMode를 true로 변경하고,
-> 해당 글의 title값과 content 값을 state로 저장하여
-> PostWritePage 컴포넌트의 props로 넘겨준다.

isModifyMode는 삼항 연산자의 조건으로 사용하여
true면 PostWritePage 컴포넌트가 보여지고,
false면 일반적인 글이 보여진다.

PostWritePage 컴포넌트는 props가 있으면 수정 모드인 것이다.
따라서 mount될 때 아래와 같은 함수가 작동하도록 했다.

useEffect(() => {
  //props로 받아온 값이 있으면 input의 value로 설정한다.
  if (modifyTitle !== undefined && modifyContent !== undefined) {
    setTitle(modifyTitle);
    setContent(modifyContent);
    setId(postId);
  }
}, []);

그리고 submit 버튼을 눌렀을 때에 글 수정/생성 모드에 따라서 fetch 요청 형식이 달라져야 하므로
submit 버튼 onClick 이벤트에 "props로 들어오는 값의 유무"에 따라 if...else문으로 로직을 분기했다.

수정 모드는 id값, 작성자, 글 최초 등록 시간, 댓글 등을 그대로 두고
title값과 content 값만을 변경하는 것이므로,
이것을 요청 바디에 담아 endpoint /post/${id} 로 PATCH 요청을 보낸다.

const modifyPost = {
  title: title,
  content: content,
};
const options = {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(modifyPost),
};
fetch(`http://localhost:3001/post/${id}`, options)
  .then(() => navigate(`/post/${id}`))
  .catch((error) => console.log("error", error));

글 수정 모드에서 요청이 완료되면 endpoint /post/${id}로 라우팅하여 수정 완료된 글을 볼 수 있도록 했다.
마찬가지로 페이지를 새로고침하여 새로운 data를 받아온다.



Delete :: 글 삭제 (DELETE)

글 삭제는 /post/${postId}로 요청 바디 없이 DELETE 요청을 보내면 돼서 비교적 간단하다.

글 삭제 버튼을 누르면 아래 함수가 작동한다.

  const deleteEvent = (e) => {
    fetch(`http://localhost:3001/post/${postId}`, {
      method: "DELETE",
    })
      .then((json) => json.text())
      .then((result) => console.log(result))
      .catch((err) => console.log(err));
    navigate("/");
    window.location.reload();
  };

삭제 후에는 메인 화면으로 이동하고 화면을 새로고침하여 data를 새로 받아온다.

Update :: 댓글 삭제 (PUT)

댓글 삭제는 PUT method를 사용한다.

위에서 언급한 것처럼 글 data의 하위 data로 댓글이 들어가 있는 모습이라, 삭제 버튼을 누르면 해당 댓글을 제외한 다른 data로 변경하는 PUT 요청을 보내는 것으로 삭제를 구현했다.

먼저 삭제 버튼의 value를 댓글 id값으로 설정해주었다.

현재 endpoint의 postId와 일치하는 id값을 가진 data 중에서
-> 삭제 버튼이 눌린 댓글의 value와, 댓글 data의 id값이 일치하는 것을 찾는다.
-> 일치하는 요소를 제외하여 댓글 배열을 필터링한다.

const newData = Object.assign({}, ...copiedData, {comments: commentDeleted}) //commentDeleted = 필터링된 댓글 배열
const options = {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(newData),
};
fetch(`http://localhost:3001/post/${postId}`, options)
  .then((res) => res.text())
  .catch((error) => console.log("error", error));

새롭게 data 객체를 만들고, 이를 요청 바디에 포함하여 /post/${postId}로 PUT 요청을 보낸다.

마찬가지로 삭제가 완료되면 페이지를 새로고침하여 새로운 data를 받아온다.



이렇게 CRUD 구현을 완료했다!




🚀 후기

사실 이틀에 걸쳐서 완성했다.
기본적인 기능은 돌아가는 미니 블로그를 만들고 싶었는데 생각한 대로 잘 나와서 뿌듯하고 기쁘다!

CSS도 열심히 꾸안꾸 스타일로 만져보고, 로딩 인디케이터도 예쁜 거로 구해와서 적용시켰다.

netlify로 배포를 시도했으나 정적 웹 사이트 배포만 가능하여 내가 로컬 환경에서 json-server을 따로 켜야 접속이 가능했다.
그리고 서버를 따로 켰는데도 fetch 요청이 원활하게 이루어지지 않았다.
아쉽다.. 나중에 AWS로 동적 웹 사이트 배포를 해보고 싶다.

후에 로그인 기능을 추가해서 실제 블로그처럼 작동되도록 만들고 싶다.

그리고 글 작성 완료하면 해당 글의 id값으로 라우팅되도록 구현하고 싶은데, 내가 등록한 글의 response data에서 id값을 받아와서 사용하려 했으나 에러가 발생하면서 잘 안됐다.
이 부분 수정하여 구현해볼 예정이다!!




😎 완성된 화면 & 깃허브 링크

https://github.com/y0ungg/S4-Solo-Project-CRUD-blog

profile
즐겁게 공부하고 꾸준히 기록하는 나의 프론트엔드 공부일지

0개의 댓글