상세 컨텐츠

본문 제목

React와 Express를 사용해 메모장 만들기(Hooks 사용 ver.)

21-22/21-22 리액트 마스터

by 도리에몽 2021. 12. 1. 23:00

본문

728x90

첫 번째 글에서는 express를 사용하지 않고, 오로지 react로만 메모장 기능을 구현해보았는데요,

이번 글에서는 express를 사용해 메모장 만들기 프로젝트를 진행해보겠습니다.

 

 https://gngsn.tistory.com/17 를 바탕으로 만들었습니다.

 

원글에서는 클래스형 컴포넌트를 사용하였는데요. 

저희는 함수형 컴포넌트를 이용해 메모장을 만들어보기로 하였으며,

새 메모장을 생성하는 기능뿐만 아니라, 기존 메모를 수정하고 삭제하는 기능까지 추가해보았습니다.

 

데이터베이스는 mongoDB를 사용하였고, 저희는 리액트를 중점적으로 볼 예정이기 때문에

API들은 간단하게 소개하겠습니다.

router.get("/", memoController.findAll); //모든 memo 불러오기
router.post("/", memoController.write); //memo 생성하기
router.get("/:memoID", memoController.readOne); //선택한 memo 읽어오기
router.put("/:memoID", memoController.update); //선택한 memo 수정하기
router.delete("/:memoID", memoController.delete); //선택한 memo 삭제하기

아래는 controller안에 들어있는 코드들입니다. 

const memo = {
  findAll: async (req, res) => {
    Memo.findAll()
      .then((memos) => {
        if (!memos.length)
          return res.status(404).send({ err: "Memo not found" });
        res.send(memos);
      })
      .catch((err) => res.status(500).send(err));
  },
  write: async (req, res) => {
    Memo.create(req.body)
      .then((memo) => res.send(memo))
      .catch((err) => res.status(500).send(err));
  },
  readOne: async (req, res) => {
    Memo.findOneByMemoId(req.params.memoID)
      .then((memo) => {
        if (!memo) return res.status(404).send({ err: "Memo not found" });
        res.send(memo);
      })
      .catch((err) => res.status(500).send(err));
  },
  update: async (req, res) => {
    Memo.updateByMemoID(req.params.memoID, req.body)
      .then((memo) => res.send(memo))
      .catch((err) => res.status(500).send(err));
  },
  delete: async (req, res) => {
    Memo.deleteByMemoID(req.params.memoID)
      .then(() => res.sendStatus(200))
      .catch((err) => res.status(500).send(err));
  },
};

 

server에서 만든 api들은 client에서 fetch함수를 이용해 호출할 것입니다.

  //서버랑 연결시켜서 데이터 불러오기
  useEffect(() => {
    fetch("/memo", {
      method: "GET",
      headers: {
        "Content-Type": "application/json:charset=UTF-8",
        Accept: "application/json",
      },
      mode: "cors",
    })
      .then((res) => {
        return res.json();
      })
      .then((memos) => {
        setMemos(memos);
        //memoID증가시켜줘야해서 데이터 받아올때 현재 개수 셈
        setMemoID(memos.length);
        console.log("Network success - memo : ", memos);
      })
      .catch((error) => console.log("Network Error : ", error));
  }, []);

db에 저장된 전체 memo가 호출된 모습

//함수 컴포넌트는 constructor가 필요하지 않다.
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [reModalOpen, setReModalOpen] = useState(false);
  const [memos, setMemos] = useState([]);
  const [memoID, setMemoID] = useState(0);
  const [clickmemo, setClickmemo] = useState({
    title: "",
    author: "",
    content: "",
    index: 0,
  });

app.js에서 useState메서드를 통해 state값을 지정할 때, 가장 먼저 클래스형과 다르다고 느낀 점은 바로 constructor가 필요하지 않다는 점입니다. 

현재 memos라는 state 배열을 만들어주었고, 이곳에 fetch함수를 통해 불러온 db들을 저장하였기 때문에, 앞선 그림처럼 db에 저장된 memo들이 나타나게 됩니다.

 

memoID는 memos 배열의 길이를 알아내어 저장함으로써, 새로운 memo가 추가될 때 해당 메모의 index값을 지정해주기 위해 존재합니다.

 

clickmemo는 기존 모달창을 open 했을 때, 몇 번째 memo를 눌렀는지 memo안의 정보는 뭐가 들었는지를 reModal 컴포넌트에 전달해주기 위해 존재합니다.

 

<Modal isOpen={isModalOpen} close={closeModal} memoID={memoID} />
<ReModal
   reOpen={reModalOpen}
   reclose={recloseModal}
   data={{ ...clickmemo }} //딥카피
   //onUpdate={handleUpdate}
   //onRemove={handleRemove}
   memoID={memoID}
/>

app.js의 state값을 props로 넘겨주는 부분입니다.

ReModal을 보면, express를 쓰지 않았을 때는 onUpdate와 onRemove 함수가 따로 필요했음을 알 수 있습니다. 

하지만, express를 쓰면 클릭과 동시에 fetch함수를 통해 api를 호출하여 db안의 내용을 수정하거나 삭제할 수 있습니다.

app.js에 호출되는 memos는 db에 저장되어있는 memos이기 때문에 수정과 삭제가 이뤄진 후 변경된 db목록들을 불러오게 됩니다.

따라서, app.js에서 기존 memo 배열을 복제하여 새로운 memo를 붙여 또 다른 배열을 생성해내거나 제거하는 과정이 필요없게 됩니다. 

 

const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [author, setAuthor] = useState("");

  const handleSubmit = (event) => {
    fetch(`/memo`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json;charset=UTF-8",
        Accept: "application/json",
      },
      mode: "cors",
      body: JSON.stringify({
        // fetch 특징
        memoID: memoID + 1,
        title: title,
        content: content,
        author: author,
      }),
    })
      .then((response) => {
        this.props.close();
        return response.json();
      })
      .catch((err) => {
        console.log(err);
      });
  };
  
    const handleChange = useCallback((event) => {
    const {
      target: { name, value },
    } = event;

    if (name === "author") {
      setAuthor(value);
    } else if (name === "title") {
      setTitle(value);
    } else if (name === "content") {
      setContent(value);
    }
  }, []);

Modal 컴포넌트의 코드입니다.

handleChange함수를 통해 input창에 입력된 문자열을 author, title, content state값으로 지정해주었고,

submit버튼을 눌렀을 때, api호출하여 저장되게 하였습니다. 

 

const ReModal = ({ reOpen, reclose, data }) => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [author, setAuthor] = useState("");
  const [thisID, setThisID] = useState(0);

  useEffect(() => {
    const id = data.index + 1;

    setThisID(id);

    //클릭한 메모 불러오는 api
    fetch(`/memo/${id}`, {
      method: "GET",
      headers: {
        "Content-Type": "application/json:charset=UTF-8",
        Accept: "application/json",
      },
      mode: "cors",
    })
      .then((res) => {
        return res.json();
      })
      .then((memo) => {
        setTitle(memo.title);
        setContent(memo.content);
        setAuthor(memo.author);
        console.log("Network success - memo : ", memo);
      })
      .catch((error) => console.log("Network Error : ", error));
  }, [data]);

ReModal 컴포넌트의 코드입니다.

 

Modal컴포넌트와 ReModal 컴포넌트는 기존에 있는 memo를 다루는지 다루지 않는지의 역할을 구분하기 위해 나눠 구현하였습니다.

Modal 컴포넌트는 오로지 새 memo를 추가하는 기능만 있으며,

Modal 컴포넌트는 기존 memo를 불러오고, 수정하거나, 삭제하는 기능만 있습니다.

 

먼저 useEffect함수를 이용해 props가 변경될 때 호출되는 오버라이딩 함수인 componentWillReceivePorps메서드 역할을 하게 했습니다. 

useEffect함수 안에서 클릭한 메모를 불러온 후, 클릭한 메모의 정보를 input 값 안에 넣어주었습니다.

 

id를 넘겨줄 때, props로 받아온 data의 index값에 +1을 해주었습니다.

app 컴포넌트에서 data의 index는 map함수의 내장 index값을 사용하였는데, db의 memoID는 1부터 시작했기 때문에 +1을 해줘야 합니다.

const handleUpdate = (event) => {
    event.preventDefault();

    const id = thisID;

    fetch(`/memo/${id}`, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json;charset=UTF-8",
        Accept: "application/json",
      },
      mode: "cors",
      body: JSON.stringify({
        // fetch 특징
        title: title,
        content: content,
        author: author,
      }),
    })
      .then((response) => {
        reclose();
        return response.json();
      })
      .catch((err) => {
        console.log(err);
      });
  };

memo를 수정하는 부분입니다.

 

const handleRemove = () => {
    const id = thisID;

    fetch(`/memo/${id}`, {
      method: "DELETE",
      headers: {
        "Content-Type": "application/json;charset=UTF-8",
        Accept: "application/json",
      },
      mode: "cors",
      body: JSON.stringify({
        // fetch 특징
        title: title,
        content: content,
        author: author,
      }),
    })
      .then((response) => {
        reclose();
        return response.json();
      })
      .catch((err) => {
        console.log(err);
      });
  };

memo를 삭제하는 부분입니다.

 

db 잘 추가되는 모습
기존 모달 클릭시 input박스 안에 값이 잘 불러와지는 모습
메모 삭제 잘되는 모습

함수형 컴포넌트로 구성해보면서, 클래스형 컴포넌트에 비해 확실히 state와 props 전달하는 것에 있어서 더 직관적이고 사용하기 편리하다는 느낌을 받았습니다. 

server 부분이 더 궁금하시다면 위의 github로 많이 놀러오세요 :)

728x90

관련글 더보기