Today Sangmin Learned
article thumbnail
728x90

이전에 Node.js + Express 환경에서 Mock data를 만들고 API를 설계하는 일까지 해봤다.

그래서 이번에는 그렇게 설계한 API를 클라이언트 단에서 API 통신을 통해 받아온 뒤 뿌려주는 것에 대해 포스팅해보려고 한다.

 

우선 async-await 함수에 사용할 fetcher 함수부터 만들어보자.

1. fetcher.js

import axios from 'axios';

axios.defaults.baseURL = 'http://localhost:8000'; // 혹은 본인의 서버 URL

const fetcher = async (method, url, ...rest) => {
  const res = await axios[method](url, ...rest);
  
  return res.data
};

export default fetcher;

API 통신을 좀더좀 더 수월하게 하기 위해 메인 컴포넌트에서 불러와서 사용할 매개체 함수이다. 일반적으로 사용하는 axios보다 좀 더 직관적이다. axios('GET/POST/PUT/DELETE 등', '/messages', { 넘겨줄 값 }); 의 형태로 사용하면 된다.

2. MsgList.jsx

이전 포스팅에서 messages.json의 값들이 messages.js를 통해 데이터 형식으로 클라이언트에 보내지도록 설정해뒀다. 그래서 이 컴포넌트에서는 서버에서 보내는 값을 받을 것이다.

import React, { useState, useEffect } from "react";
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import fetcher from "../fetcher.js";

const msgList = () => {
  const [msgs, setMsgs] = useState([]);
  const editingId, setEditingId] = useState(null);

  const {
    query: { userId = "" },
  } = useRouter();

  const onCreate = async (text) => {
    const newMsg = await fetcher("post", "/messages", { text, userId });
    if (!newMsg) throw Error("Error!");
    setMsgs((msgs) => [newMsg, ...msgs]);
  };

  const onUpdate = async (text, id) => {
    const newMsg = await fetcher("put", `/messages/${id}`, { text, userId });
    if (!newMsg) throw Error("something wrong");
    setMsgs((msgs) => {
      const targetIdx = msgs.findIndex((msg) => msg.id === id);
      if (targetIdx < 0) return msgs;
      const newMsgs = [...msgs];
      newMsgs.splice(targetIdx, 1, newMsg);
      return newMsgs;
    });
    doneEdit();
  };

  const doneEdit = () => setEditingId(null);
  
  const getMessages = async () => {
    const msgs = await fetcher("get", "/messages");
    setMsgs(msgs);
  };
  
  useEffect(() => {
    getMessages();
    console.log('서버 연결 완료');
  }, []);
  
  const onDelete = async (id) => {
    const receivedId = await fetcher("delete", `/messages/${id}`, {
      params: { useId },
    });
    
    setMsgs((msgs) => {
      const targetIdx = msgs.findIndex((msg) => msg.id === receivedId + "");
      if (targetIdx < 0) return msgs;
      const newMsgs = [...msgs];
      newMsgs.splice(targetIdx, 1);
      return newMsgs;
    });
  };
  
  return (
    <>
      {userId && <MsgInput mutate={onCreate} />}
      <ul className="messages">
        {msgs.map((x) => (
          <MsgItem
           key={x.id}
           {...x}
           onUpdate={onUpdate}
           onDelete={() => onDelete(x.id)}
           startEdit={() => setEditingId(x.id)}
           isEditing={editingId === x.id}
           myId={userId}
        )
      </ul>
    </>
  );
}

export default msgList;

내용이 길어서, 하나씩 차근차근 보겠다.

2-1. getMessages(GET)

가장 기본인 GET 메소드의 사용부터 보자.

  const getMessages = async () => {
    const msgs = await fetcher("get", "/messages");
    setMsgs(msgs);
  };
  (..)
  useEffect(() => {
    getMessages();
    console.log('서버 연결 완료');
  }, []);

위에서 설명한 fetcher함수를 사용하여 GET 메소드로 '/messages'(정확히는 'http://localhost:8000/messages')의 값을 가져온 뒤 그 값을 담은 변수를 useState를 사용해서 msgs 상태에 부여한다. 그리고 아래에 useEffect를 통해 처음 이 컴포넌트의 지배를 받는 URL에 들어갔을 때에 getMessages 함수를 실행하고 '서버 연결 완료'를 콘솔에 출력하게끔 하였다. 이 8000번 포트의 messages를 가져오려면 당연하게도 서버가 켜져있어야 한다. 그렇기에 API 통신을 테스트해보려고 한다면 무조건 서버와 클라이언트 모두 켜져있어야 한다.

 

2-2. onCreate(POST)

  const onCreate = async (text) => {
    const newMsg = await fetcher("post", "/messages", { text, userId });
    if (!newMsg) throw Error("Error!");
    setMsgs((msgs) => [newMsg, ...msgs]);
  };

이제 두번째로 POST이다. 우리가 새롭게 적은 text와 userId의 값을 POST 메소드를 통해 localhost:8000/messages로 보낸 다음 msgs를 새로운 메시지(newMsg)와 기존에 있던 나머지 메시지들(... msgs)을 합쳐서 설정하게 하였다. 중간에 newMsg가 없을 때 에러를 띄우는 이유는, 아무 값도 없는 것을 보냈을 때에 공백으로 처리가 되어 불필요한 데이터가 발생하는 것을 방지하기 위함이다.

 

2-3. onUpdate(PUT)

  const onUpdate = async (text, id) => {
    const newMsg = await fetcher("put", `/messages/${id}`, { text, userId });
    if (!newMsg) throw Error("something wrong");
    setMsgs((msgs) => {
      const targetIdx = msgs.findIndex((msg) => msg.id === id);
      if (targetIdx < 0) return msgs;
      const newMsgs = [...msgs];
      newMsgs.splice(targetIdx, 1, newMsg);
      return newMsgs;
    });
    doneEdit();
  };

const doneEdit = () => setEditingId(null);

다른 메소드들에 대한 함수와 다르게 백틱(`)을 사용하였다. 그 이유는 가변값인 id가 들어가기 때문이다. 저렇게 변하는 값을 다뤄야 할 때에는 백틱 내부에 ${}와 같은 형태로 작성해주면 된다. 그리고 보낼 값은 당연히 POST 메소드에서와 마찬가지로 text와 userId이다.

이 부분은 지난 포스팅에서의 PUT 함수와 크게 다르지 않다. 메시지라는, messages.json 내부의 데이터 각각이 가진 id와 내가 수정하려는 id가 같은 때의 인덱스를 구한 다음 그 값이 -1이라면(없다면) 기존 메시지를 반환하고, 그게 아니라면 지난 포스팅에서 얘기했던 대로 targetIdx 내부의 값을 newMsg로 바꿔준 다음 return 하는 것이다.

 

2-4. onDelete(DELETE)

  const onDelete = async (id) => {
    const receivedId = await fetcher("delete", `/messages/${id}`, {
      params: { userId },
    });
    setMsgs((msgs) => {
      const targetIdx = msgs.findIndex((msg) => msg.id === receivedId + "");
      if (targetIdx < 0) return msgs;
      const newMsgs = [...msgs];
      newMsgs.splice(targetIdx, 1);
      return newMsgs;
    });
  }0;

onUpdate와 거의 똑같지만 splice에 대체값이 없다는 차이점이 있다.

 

6행의 receivedId + ""이 있는데, 이 ""을 더해준 이유는 문자열로 바꿔주기 위함이다. 기본적으로 우리가 messages.json에서 값들을 전부 문자열로 지정을 해줬었는데, V4를 통해 랜덤으로 부여된 아이디는 무조건 문자열 처리가 되어있지만 기본적으로 우리가 처음에 지정해준 id("3", "50" 등)는 문자열로 지정이 되긴 했으나 파싱 과정에서 숫자로 인식이 될 여지가 있다. 그렇기 때문에 ""을 더해줌으로써 문자열로 바꿔준 것이다.

 

3. MsgItem 컴포넌트와 MsgInput 컴포넌트

MsgList는 메시지 리스트들을 띄워주는 컴포넌트인데, return문을 보면 MsgItem이 있을 것이다. 이는 이 텍스트들을 직접적으로 띄워주는 컴포넌트라고 생각하면 된다. 스타일링 파일까지 보여주긴 너무 글이 길어져서 짧게 하면 상단의 텍스트 박스 및 완료 버튼(MsgInput 컴포넌트), 그리고 각 텍스트 별로 띄워주면서 수정/삭제를 보여주는 기능을 가진 컴포넌트이다.

 

더보기
import React from "react";
import MsgInput from "./MsgInput";

const MsgItem = ({
  id,
  userId,
  timestamp,
  text,
  onUpdate,
  isEditing,
  onDelete,
  startEdit,
  myId,
}) => {
  return (
    <li className="messages__item">
      <h3>
        {userId}{" "}
        <sub>
          {new Date(timestamp).toLocaleString("ko-KR", {
            year: "numeric",
            month: "numeric",
            day: "numeric",
            hour: "2-digit",
            minute: "2-digit",
            hour12: true,
          })}
        </sub>
      </h3>
      {isEditing ? (
        <>
          <MsgInput text={text} mutate={onUpdate} id={id} />
        </>
      ) : (
        text
      )}

      {myId === userId && (
        <div className="messages__buttons">
          <button onClick={startEdit}>수정</button>
          <button onClick={onDelete}>삭제</button>
        </div>
      )}
    </li>
  );
};

export default MsgItem;
더보기
import React, { useRef } from "react";

const MsgInput = ({ mutate, text = "", id = undefined }) => {
  const textRef = useRef(null);

  const onSubmit = (e) => {
    e.preventDefault();
    e.stopPropagation();
    const text = textRef.current.value;
    textRef.current.value = "";
    mutate(text, id);
  };
  return (
    <form className="messages__input" onSubmit={onSubmit}>
      <textarea
        defaultValue={text}
        ref={textRef}
        placeholder="내용을 입력하세요."
      />
      <button type="submit">완료</button>
    </form>
  );
};

export default MsgInput;

여기에서 중요한 점은 UI 처리인데, localhost:3000/?userId=steadily 로 했을 경우 userId가 steadily인 데이터만 보여주고, 반대로 worked로 했을 경우 userId가 worked인 데이터만 보여주게 해두었다. MsgList.jsx에서는 userId가 있을 경우에만 MsgInput 컴포넌트를 띄움으로써 userId가 없다면 데이터를 추가할 수 없게 해두었다.

 

그리고, MsgItem.jsx를 보면 삼항 연산자를 통해 isEditing일 경우에만 MsgInput 컴포넌트가 보이게끔 하였다. 위에서와의 차이라면 수정/삭제가 가능하게끔 기존 data가 들어가 있다는 것인데, 이 isEditing 상태는 부모 컴포넌트인 MsgList.jsx에서 관리한다. editingId와 x.id가 같을 경우에 isEditing이 true가 되는 것이다. 그렇다면 이 editingId는 어디서 가져오는가 하면 바로 그 윗줄에서 가져온다. startEdit, 즉 수정을 할 수 있는 상태의 경우 MsgItem.jsx에 있는 수정 버튼을 클릭했을 때 그 startEdit 함수가 실행이 되는 것이다.

 

줄여서 얘기하면 데이터에 커서를 올렸을 때 수정/삭제를 할 수 있는 경우는 localhost:3000/?userId="OOO"와 그 데이터의 userId가 동일한 경우이고, 이 때에 수정 버튼을 누르면 startEdit 함수가 실행 -> 그 데이터 내부에 있는 id로 editingId를 설정(setEditingId) -> 데이터 내부에 있는 id와 editingId가 같은 경우 isEditing 상태 설정 -> MsgItem.jsx에서 isEditing 상태일 경우에만 수정/삭제 기능을 사용할 수 있다고 했으므로 이 때에 수정/삭제 기능 활성화(기존에 data의 text는 그대로 가져오면서 거기에서 값을 수정할 수 있음) 의 과정인 것이다.

 

무한스크롤과 SSR 등등 더 작성할 게 많지만 글이 너무 길어져서 여기까지만 한다..

위의 과정을 전부 마친 완성본이다.

 

profile

Today Sangmin Learned

@steadily-worked

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!