6 분 소요


1. 설정 및 세팅

1.1 환경 설정

axios 환경 설치 / json-server 환경 설치

yarn add axios json-server


1.2 db.json 파일 생성

{
  "todos": [
    {
      "id": "1",
      "title": "오늘 뭐하지?",
      "contents": "리액트 공부하자!",
      "isDone": true
    },
    {
      "id": "2",
      "title": "저녁 뭐 먹지",
      "contents": "감자탕 먹고싶당",
      "isDone": true
    }
  ]
}


1.3 json-server 실행

npx json-server db.json --port [원하는 포트]



2. 프로세스: data를 redux로 관리하기⭐

① useEffect로 데이터 가져오기

컴포넌트 마운트 시 useEffect를 사용해 비동기로 서버에서 데이터를 가져오고(axios), dispatch로 Redux 상태를 초기화한다.

useEffect(() => {
  const getTodos = async () => {
    try {
      const { data } = await axios.get("http://localhost:4000/todos");
      dispatch(initialization(data));
    } catch (error) {
      console.error(error);
    }
  };
  getTodos();
}, []);

② dispatch 사용하기

서버에서 데이터를 가져온 후 initialization 액션을 dispatch해 Redux 스토어를 초기화한다.

dispatch(initialization(data));

③ useSelector로 상태 읽기

useSelector를 사용해 Redux 스토어의 todos 상태를 컴포넌트에서 읽어온다.

const todos = useSelector((state) => state.todos);

④ 추가 Click

  • 새로운 할 일을 추가하는 함수에서 axios로 서버에 POST 요청을 보내고, dispatchaddTodo 액션을 호출해 Redux 상태를 업데이트한다.
    • dispatch(클라이언트 업데이트)를 안하고 addDoc(db 업데이트)만 했을 때, 추가된 항목이 안보임
    • addDoc을 안하고 dispatch만 했을 때, DB에 저장이 안됨 -> 새로고침시 문제

⭐ addDoc과 dispatch를 일치 시키는게 중요하다!(스프레드 연산자 사용하기)
💡 addDoc과 dispatch를 한꺼번에 처리할 수 있는게 “RTK”이다.

const newTodo = { title, contents, isDone: false };

try {
  const response = await axios.post("http://localhost:4000/todos", newTodo);
  const newId = response.data.id;
  dispatch(addTodo({ ...newTodo, id: newId }));
  setTitle("");
  setContents("");
} catch (error) {
  alert("서버 통신 중 에러가 발생했습니다. 다시 시도해주세요.");
  console.error(error);
}
  • 삭제와 수정도 마찬가지로 remove Doc or Update Docdispatch를 같이 해줘야 한다!
  • ⭐ id를 db에 있는 값으로 재구성 시키는 작업을 하는 것이 중요하다.
    • (기존에 있던 객체를 스프레드 연산자로 펼친다음에 id 부여할 것!)



3. 구현하기

3.1 module 변경

import { v4 as uuidv4 } from "uuid";

// action items
const ADD_TODO = "ADD_TODO";
const REMOVE_TODO = "REMOVE_TODO";
const SWITCH_TODO = "SWITCH_TODO";
const INITIALIZATION = "initialization"; // 기존의 todos state를 모두 지워버리고, 외부 DB로부터 가져온 값으로 갱신

/* todo 객체를 입력받아, 기존 todolist에 더함 */
export const addTodo = (payload) => {
  return {
    type: ADD_TODO,
    payload,
  };
};

/* todo의 id를 입력받아, 일치하는 todolist를 삭제 */
export const removeTodo = (payload) => {
  return {
    type: REMOVE_TODO,
    payload,
  };
};

/* todo의 id를 입력받아, 일치하는 todo 아이템의 isDone을 반대로 변경 */
export const switchTodo = (payload) => {
  return {
    type: SWITCH_TODO,
    payload,
  };
};

/* 초기화 메서드, 즉 외부 DB로 부터 가져온 todo의 <List> */
export const initialization = (payload) => {
  return {
    type: INITIALIZATION,
    payload,
  };
};

// initial states
const initialState = [];

// reducers
const todos = (state = initialState, action) => {
  switch (action.type) {
    case INITIALIZATION:
      return action.payload;

    case ADD_TODO: // 기존의 배열에 입력받은 객체를 더함
      return [...state, action.payload];

    case REMOVE_TODO: // 기존의 배열에서 입력받은 id의 객체를 제거(filter)
      return state.filter((item) => item.id !== action.payload);

    case SWITCH_TODO: // 기존의 배열에서 입력받은 id에 해당하는 것만 isDone을 반대로 변경(아니면 그대로 반환)
      return state.map((item) => {
        if (item.id === action.payload) {
          return { ...item, isDone: !item.isDone };
        } else {
          return item;
        }
      });
    default:
      return state;
  }
};

// export
export default todos;


3.2 TodoList 컴포넌트 변경

3.2.1 데이터 초기화, 조회 변경

import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { StyledDiv, StyledTodoListHeader, StyledTodoListBox } from "./styles";
import Todo from "../Todo";
import axios from "axios";
import { initialization } from "../../modules/todos";

/* 컴포넌트 개요 : 메인 > TODOLIST. 할 일의 목록을 가지고 있는 컴포넌트 */
function TodoList({ isActive }) {
  const dispatch = useDispatch();

  useEffect(() => {
    const getTodos = async () => {
      try {
        const { data } = await axios.get("http://localhost:4000/todos");
        dispatch(initialization(data));
      } catch (error) {
        console.error(error);
      }
    };

    getTodos();
  }, []);

  const todos = useSelector((state) => state.todos);

  return (
    <StyledDiv>
      <StyledTodoListHeader>
        {isActive ? "해야 할 일 ⛱" : "완료한 일 ✅"}
      </StyledTodoListHeader>
      <StyledTodoListBox>
        {todos
          .filter((item) => item.isDone === !isActive)
          .map((item) => {
            return <Todo key={item.id} todo={item} isActive={isActive} />;
          })}
      </StyledTodoListBox>
    </StyledDiv>
  );
}

export default TodoList;


3.2.2 switch, delete 변경

import React from "react";
import { useDispatch } from "react-redux";
import HeightBox from "../common/HeightBox";
import { removeTodo, switchTodo } from "../../modules/todos";
import { useNavigate } from "react-router-dom";
import {
  StyledDiv,
  StyledTitle,
  StyledContents,
  TodoButton,
  FlexButtonBox,
  LinkedP,
  FlexTitleBox,
} from "./styles";
import axios from "axios";

/* 메인 > TODOLIST > TODO. 할 일의 단위 컴포넌트 */
function Todo({ todo, isActive }) {
  // 삭제 확인 용 메시지 관리
  const CONFIRM_MESSAGE = `[삭제 확인]\n\n"${todo.title}" 항목을 정말로 삭제하시겠습니까?\n삭제를 원치 않으시면 [취소] 버튼을 눌러주세요.`;

  // hooks
  const dispatch = useDispatch();
  const navigate = useNavigate();

  // 완료, 취소를 handling하는 함수
  const handleSwitchButton = async () => {
    // 서버 통신 먼저!
    try {
      await axios.patch(`http://localhost:4000/todos/${todo.id}`, {
        isDone: !todo.isDone,
      });
      dispatch(switchTodo(todo.id));
    } catch (error) {
      alert("서버 통신 중 에러가 발생했습니다. 다시 시도해주세요.");
    }
  };

  // [삭제] 버튼 선택 시 호출되는 함수(user의 confirmation 필요)
  const handleRemoveButton = async () => {
    if (window.confirm(CONFIRM_MESSAGE)) {
      try {
        // 서버 통신 먼저!!
        await axios.delete(`http://localhost:4000/todos/${todo.id}`);
        dispatch(removeTodo(todo.id));
      } catch (error) {
        alert("서버 통신 중 에러가 발생했습니다. 다시 시도해주세요.");
        console.error(error);
      }
    }
  };

  // [상세보기]를 선택하는 경우 이동하는 함수
  const handleDetailPageLinkClick = () => {
    navigate(`/${todo.id}`);
  };

  return (
    <StyledDiv>
      <FlexTitleBox>
        <StyledTitle>{todo.title}</StyledTitle>
        <LinkedP onClick={handleDetailPageLinkClick}>[상세보기]</LinkedP>
      </FlexTitleBox>
      <HeightBox height={10} />
      <StyledContents>{todo.contents}</StyledContents>
      <HeightBox height={20} />
      <FlexButtonBox>
        <TodoButton onClick={handleSwitchButton}>
          {isActive ? "완료" : "취소"}
        </TodoButton>
        <TodoButton onClick={handleRemoveButton}>삭제</TodoButton>
      </FlexButtonBox>
    </StyledDiv>
  );
}

export default Todo;


3.2.3 Input 컴포넌트 변경

import React, { useState } from "react";
import LabledInput from "../common/LabledInput";
import HeightBox from "../common/HeightBox";
import { StyledButton } from "./styles";
import { FlexDiv } from "./styles";
import RightMarginBox from "../common/RightMarginBox";
import "./styles";
import { StyledDiv } from "./styles";
import { useDispatch, useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import { addTodo } from "../../modules/todos";
import axios from "axios";

/* Todo 메인 페이지에서 제목과 내용을 입력하는 영역 */
function Input() {
  const dispatch = useDispatch();

  // useSelector를 통한, store의 값 접근
  const todos = useSelector((state) => state.todos);

  // 컴포넌트 내부에서 사용할 state 2개(제목, 내용) 정의
  const [title, setTitle] = useState("");
  const [contents, setContents] = useState("");

  // 에러 메시지 발생 함수
  const getErrorMsg = (errorCode, params) => {
    switch (errorCode) {
      case "01":
        return alert(
          `[필수 입력 값 검증 실패 안내]\n\n제목과 내용은 모두 입력돼야 합니다. 입력값을 확인해주세요.\n입력된 값(제목 : '${params.title}', 내용 : '${params.contents}')`
        );
      case "02":
        return alert(
          `[내용 중복 안내]\n\n입력하신 제목('${params.title}')및 내용('${params.contents}')과 일치하는 TODO는 이미 TODO LIST에 등록되어 있습니다.\n기 등록한 TODO ITEM의 수정을 원하시면 해당 아이템의 [상세보기]-[수정]을 이용해주세요.`
        );
      default:
        return `시스템 내부 오류가 발생하였습니다. 고객센터로 연락주세요.`;
    }
  };

  // title의 변경을 감지하는 함수
  const handleTitleChange = (event) => {
    setTitle(event.target.value);
  };

  // contents의 변경을 감지하는 함수
  const handleContentsChange = (event) => {
    setContents(event.target.value);
  };

  // form 태그 내부에서의 submit이 실행된 경우 호출되는 함수
  const handleSubmitButtonClick = async (event) => {
    // submit의 고유 기능인, 새로고침(refresh)을 막아주는 역함
    event.preventDefault();

    // 제목과 내용이 모두 존재해야만 정상처리(하나라도 없는 경우 오류 발생)
    // "01" : 필수 입력값 검증 실패 안내
    if (!title || !contents) {
      return getErrorMsg("01", { title, contents });
    }

    // 이미 존재하는 todo 항목이면 오류
    const validationArr = todos.filter(
      (item) => item.title === title && item.contents === contents
    );

    // "02" : 내용 중복 안내
    if (validationArr.length > 0) {
      return getErrorMsg("02", { title, contents });
    }

    // 추가하려는 todo를 newTodo라는 객체로 새로 만듦
    const newTodo = {
      title,
      contents,
      isDone: false,
    };

    try {
      const resposne = await axios.post("http://localhost:4000/todos", newTodo);
      // 서버에서 받은 id를 추가
      const newId = resposne.data.id;
      // todo를 추가하는 reducer 호출
      // 인자 : payload
      dispatch(addTodo({ ...newTodo, id: newId }));
      // state 두 개를 초기화
      setTitle("");
      setContents("");
    } catch (error) {
      alert("서버 통신 중 에러가 발생했습니다. 다시 시도해주세요.");
      console.error(error);
    }
  };

  return (
    <StyledDiv>
      <form onSubmit={handleSubmitButtonClick}>
        <FlexDiv>
          <RightMarginBox margin={10}>
            <LabledInput
              id="title"
              label="제목"
              placeholder="제목을 입력해주세요."
              value={title}
              onChange={handleTitleChange}
            />
            <HeightBox height={10} />
            <LabledInput
              id="contents"
              label="내용"
              placeholder="내용을 입력해주세요."
              value={contents}
              onChange={handleContentsChange}
            />
          </RightMarginBox>
          <StyledButton type="submit">제출</StyledButton>
        </FlexDiv>
      </form>
    </StyledDiv>
  );
}

export default Input;

일반적으로는 서버가 id를 생성하고, 클라이언트에게 반환해주는 방식이 더 안전하고 일관성이 있다.


카테고리:

업데이트:

댓글남기기