스파르타코딩클럽 내일배움캠프 TIL34

한재창·2022년 12월 15일
0
post-thumbnail

ToDoList with Redux

저번주에 만들었던 TodoList를 리덕스를 이용하여 만들어보았다.
폴더랑 파일이 몇 개 없어서 리덕스를 사용하지 않아도 됐지만 리덕스를 공부하기 위해 이번에 숙제로 내주신 것 같다.
물론 많이 어려웠고, 내일이나 주말에 한 번 더 복습해 볼 예정이다.

폴더 구성

  • 스타일드 컴포넌트를 사용해서 css 파일은 모두 지워주었다. 깔끔해서 보기 좋다.
  • src 폴더
    • components, redux, shared, pages 폴더로 나누어 주었다.
    • 폴더에서 또 기능마다 작동하는 파일들로 분리했다.

index.js

  • Redux를 사용하기 위해 App 컴포넌트를 Provider 컴포넌트로 감싸주었다.
  • Provider의 props로 store를 준다.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

import { Provider } from "react-redux";
import store from "./redux/config/configStore";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

reportWebVitals();

App.js

  • 맨 처음에 보여지는 화면 Router를 컴포넌트로 사용하였다.
import React from "react";
import Router from "./shared/Router";

const App = () => {
  return <Router />;
};

export default App;

Router.js

  • Router 에서는 react-router-dom 을 사용해서 페이지마다 주소를 주었다.
  • path 속성이 뒤에 붙는 주소이고, 저 주소로 가면 Home 컴포넌트를 띄어준다.
  • path 속성 중에 "detail/:id" 가 있는데, useParms() 를 사용하기 위함이다.
  • react-router-dom 에 대해서는 공부를 더 해야 할 것 같다. 아직까지 이해를 다 하지 못했다..
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";

import Home from "../pages/Home";
import Detail from "../pages/Detail";

const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="detail" element={<Detail />} />
        <Route path="detail/:id" element={<Detail />} />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;

Home.jsx

  • TodoBoard 와 TodoHeader 컴포넌트를 사용한다.
  • 화면에 나타내기 위한 파일이다.
import React from "react";
import styled from "styled-components";

import TodoBoard from "../components/TodoBoard";
import TodoHeader from "../components/TodoHeader";

const Wrap = styled.div`
  max-width: 1200px;
  min-height: 800px;
  margin: 0 auto;
`;

export default function Home() {
  return (
    <Wrap>
      <TodoHeader />
      <TodoBoard />
    </Wrap>
  );
}

Detail.jsx

  • DetailsItem 컴포넌트를 사용한다.
  • 화면에 나타내기 위한 파일이다.
import React from "react";
import styled from "styled-components";

import DetailsItem from "../components/DetailsItem";

const Wrap = styled.div`
  border: 2px solid rgb(238, 238, 238);
  width: 100%;
  height: 100vh;
  display: flex;
  -webkit-box-align: center;
  align-items: center;
  -webkit-box-pack: center;
  justify-content: center;
`;

export default function Detail() {
  return (
    <Wrap>
      <DetailsItem />
    </Wrap>
  );
}

configStore.js

  • combineReducers 객체 값으로 모듈에 있는 리듀서 함수 이름(여기에서는 todos)을 주면 모듈과 연결된다.
  • createStore를 이용하여 global store를 만들어준다.
import { createStore } from "redux";
import { combineReducers } from "redux";
import todos from "../modules/todos";

// store와 module 연결해주기
const rootReducer = combineReducers({
  todos,
});

const store = createStore(rootReducer);

export default store;

todos.js

  • Action Value 에는 상수로 값을 정해준다.
  • Action Creator
    • 유지보수의 효율성 증가, 코드가독성, 오타방지를 위해서 사용
    • export 해서 dispatch() 의 값으로 받아오려고 사용
  • Initial State : 기본값
  • Reducer
    • 변화를 일으키는 함수 (ex: 1을 더함)
    • 매개변수 : state = initialState(기본값), action 을 넘겨준다고 할당해주어야 한다.
    • 리듀서는 우리가 action을 일으켰을 때 자동 실행된다.
    • 즉, store에 있는 데이터를 바꿔 주는 역할을 한다!!
    • 꼭 export default 해주고 configStore 에서 연결해 줘야한다.
// Action Value
const ADD_TODO = "ADD_TODO";
const DELETED_TODO = "DELETED_TODO";
const CHANGED_TODO = "CHANGED_TODO";

// Action Creator

// 추가
export const addItem = (payload) => {
  return {
    type: ADD_TODO,
    payload,
  };
};

// 삭제
export const deletedItem = (payload) => {
  return {
    type: DELETED_TODO,
    payload,
  };
};

// 바꾸기
export const changedItem = (payload) => {
  return {
    type: CHANGED_TODO,
    payload,
  };
};

// Initial State
const initialState = [
  { id: "1", title: "리액트 복습하기", text: "열심히 하자", isDone: false },
  { id: "2", title: "TodoList 복습하기", text: "가즈아", isDone: true },
];

// Reducer
const todos = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TODO:
      return [...state, action.payload];
    case DELETED_TODO:
      return state.filter((item) => item.id !== action.payload);
    case CHANGED_TODO:
      return state.map((item) => {
        if (item.id === action.payload)
          return { ...item, isDone: !item.isDone };
        else return item;
      });
    default:
      return state;
  }
};

export default todos;

TodoHeader.jsx

  • 머릿말을 위한 파일이며 기능은 없다.
import React from "react";
import styled from "styled-components";

const Header = styled.div`
  padding: 0 10px;
  display: flex;
  justify-content: space-between;
  border: 1px solid black;
`;

const HeaderTitle = styled.h3`
  font-weight: 800;
`;

const TodoHeader = () => {
  return (
    <Header>
      <HeaderTitle>My Todo List</HeaderTitle>
      <HeaderTitle>React</HeaderTitle>
    </Header>
  );
};

export default TodoHeader;

TodoBoard.jsx

  • 리덕스를 사용하기 전에는 이 파일에서 props를 전해줄 useState를 생성해 주었는데, 그럴 필요가 없어졌다!!
import TodoInput from "./TodoInput";
import TodoList from "./TodoList";

export default function TodoBoard() {
  return (
    <div>
      <TodoInput />
      <TodoList isWorking={true} />
      <TodoList isWorking={false} />
    </div>
  );
}

TodoInput.jsx

  • useDispatch() 를 사용하기 위해서 import 해준다.
  • dispatch 로 액션 객체를 보내주기 위해 미리 만들어 둔 액션 객체인 addItem 을 import 해준다.
  • setState() 였던 부분을 dispatch(addItem(newTodos)) 로 바꿔준다.
  • dispatch(addItem(newTodos)) : newTodos 는 payload 의 값으로 전송된다.
  • 나머지는 지역변수를 써서 전의 코드와 같다.
import React, { useState } from "react";
import styled from "styled-components";
import { v4 as uuidv4 } from "uuid";
import { useDispatch } from "react-redux";

import { addItem } from "../redux/modules/todos";

const Form = styled.form`
  display: flex;
  background-color: #eee;
  border-radius: 12px;
  justify-content: space-between;
  margin: 0 auto;
  padding: 30px;
`;

const FormInputBox = styled.div`
  align-items: center;
  display: flex;
  gap: 20px;
`;

const FormLabel = styled.label`
  font-size: 16px;
  font-weight: 700;
`;

const FormInput = styled.input.attrs({ type: "text" })`
  border: none;
  border-radius: 12px;
  height: 40px;
  padding: 0 12px;
  width: 240px;
`;

const FormButton = styled.button`
  background-color: teal;
  border: none;
  border-radius: 10px;
  color: #fff;
  font-weight: 700;
  height: 40px;
  width: 140px;
  cursor: pointer;
`;

export default function TodoInput() {
  const [titleValue, setTitleValue] = useState("");
  const [textValue, setTextValue] = useState("");
  const dispatch = useDispatch();

  // 제목 입력값을 변경해주는 이벤트
  const onChangeTitleHandler = (event) => {
    setTitleValue(event.target.value);
  };

  // 내용 입력값을 변경해주는 이벤트
  const onChangeTextHandler = (event) => {
    setTextValue(event.target.value);
  };

  // 추가하기 이벤트
  const onSubmitHandler = (event) => {
    event.preventDefault();

    const newTodos = {
      id: uuidv4(),
      title: titleValue,
      text: textValue,
      isDone: false,
    };

    if (!titleValue || !textValue)
      return alert("제목이나 내용을 입력해주세요.");

    dispatch(addItem(newTodos));

    setTitleValue("");
    setTextValue("");
  };

  return (
    <Form onSubmit={onSubmitHandler}>
      <FormInputBox>
        <FormLabel htmlFor="title">제목</FormLabel>
        <FormInput
          id="title"
          value={titleValue}
          onChange={onChangeTitleHandler}
        />
        <FormLabel htmlFor="text">내용</FormLabel>
        <FormInput id="text" value={textValue} onChange={onChangeTextHandler} />
      </FormInputBox>
      <FormButton>추가하기</FormButton>
    </Form>
  );
}

TodoList.jsx

  • useSelector() 을 사용하여 전역 스토어에 있는 state 를 가져온다.
  • 가져온 state 를 사용하여 메서드를 사용해 item 들을 화면에 띄어준다.
import React from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";

import TodoItem from "./TodoItem";

const TodoListBox = styled.div`
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
`;

export default function TodoList({ isWorking }) {
  const todos = useSelector((state) => state.todos);

  return (
    <div>
      <h1>{isWorking ? "Working" : "isDone"}</h1>
      <TodoListBox>
        {todos
          .filter((todo) => (todo.isDone ? !isWorking : isWorking))
          .map((todo) => (
            <TodoItem key={todo.id} todo={todo} />
          ))}
      </TodoListBox>
    </div>
  );
}

TodoItem.jsx

  • Link 훅을 사용하여 페이지를 이동하게 하였다.
  • setState() 를 dispatch() 로 바꿔준 부분만 빼면 똑같다.
import React from "react";
import styled from "styled-components";
import { useDispatch } from "react-redux";
import { Link } from "react-router-dom";

import { deletedItem, changedItem } from "../redux/modules/todos";

const TodoItemBox = styled.div`
  border-radius: 12px;
  padding: 12px 24px 24px;
  border: 4px solid lightblue;
  width: 270px;
`;

const BtnBox = styled.div`
  display: flex;
  gap: 10px;
  margin-top: 24px;
`;

const TodoBtn = styled.button`
  cursor: pointer;
  height: 40px;
  width: 50%;
  border-radius: 12px;
  background-color: #fff;
  border: 2px solid ${(props) => props.borderColor};
`;

export default function TodoItem({ todo }) {
  const dispatch = useDispatch();

  // 삭제 기능
  const onClickDeleteHandler = () => {
    dispatch(deletedItem(todo.id));
  };

  // isDone 바꾸기 기능
  const onClickChangeHandler = () => {
    dispatch(changedItem(todo.id));
  };

  return (
    <TodoItemBox>
      <Link to={`/detail/${todo.id}`}>상세정보</Link>
      <h1>{todo.title}</h1>
      <p>{todo.text}</p>
      <BtnBox>
        <TodoBtn borderColor="red" onClick={onClickDeleteHandler}>
          삭제
        </TodoBtn>
        <TodoBtn borderColor="green" onClick={onClickChangeHandler}>
          {todo.isDone ? "취소" : "확인"}
        </TodoBtn>
      </BtnBox>
    </TodoItemBox>
  );
}

DetailsItem.jsx

  • useSelector() 를 이용해서 state 를 가져온다.
  • useParms() 를 사용해서 조건에 맞는 id의 내용들만 화면에 띄어준다.
import React from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
import { Link, useParams } from "react-router-dom";

const Wrap = styled.div`
  width: 600px;
  height: 400px;
  border: 1px solid rgb(238, 238, 238);
  display: flex;
  flex-direction: column;
  -webkit-box-pack: justify;
`;

const HeaderWrap = styled.div`
  display: flex;
  height: 80px;
  -webkit-box-pack: justify;
  justify-content: space-between;
  padding: 0px 24px;
  -webkit-box-align: center;
  align-items: center;
`;

const BackBtn = styled.button`
  border: 1px solid rgb(221, 221, 221);
  height: 40px;
  width: 120px;
  background-color: rgb(255, 255, 255);
  border-radius: 12px;
  cursor: pointer;
`;

const BodyWrap = styled.div`
  padding: 24px;
`;
export default function DetailsItem() {
  const items = useSelector((state) => state.todos);

  const param = useParams();

  const item = items.find((item) => item.id === param.id);
  return (
    <Wrap>
      <HeaderWrap>
        <h3>ID : {item.id.slice(0, 5)}</h3>
        <Link to="/">
          <BackBtn>이전으로</BackBtn>
        </Link>
      </HeaderWrap>
      <BodyWrap>
        <h1>{item.title}</h1>
        <h2>{item.text}</h2>
      </BodyWrap>
    </Wrap>
  );
}

느낀점

  • Error

    • import addItem 을 해줄때 중괄호를 안 써줘서 결국에는 export default 를 한 todos를 import 해준 꼴이었다…
    • export default 는 import 할 때 이름을 바꿔줘도 된다.
    • 별 것 아닌 에러였지만 하나 배웠다!!
    • 그리고 인터넷을 찾아서 호이스팅 문제로 해결도 했지만 configStore.js 파일과 todos.js 파일을 분리할 수 없어 이렇게 해결하는게 훨씬 간단하고 좋은 방법이다.
  • 리덕스를 어떻게 사용해야 하는지 ?

    • 사진을 보면 TodoList.jsx 에서 props 를 받아왔는데 리덕스를 사용하는데 굳이 받아와야 하나? 라는 의문점이 들었다.
    • 그럼에도 불구하고 props 를 쓴 이유
      • useSelector() 로 state 를 가져오면 배열상태라 각각의 아이템 아이디에 접근할 수가 없었다.
      • 다른 방법이 있으나 어렵고, 원장님도 이 방법을 추천하셨다.
      • 리덕스를 쓰는 이유는 props driling 때문인데, 이 경우에는 props 를 1번 밖에 내려주지 않아 쓰는게 효율적이라고 하셨다.
    • 아직 컴포넌트의 개수가 작아서 리덕스를 사용하는데 큰 이점은 못 느꼈지만 새로운 코드를 배웠다. 성장하는 나 칭찬한다🐷
profile
취준 개발자

0개의 댓글