본문 바로가기
매일 해내는 개발/React

[React] Redux, React-router-dom으로 TodoList만들기

by 해야지 2022. 12. 15.
반응형

1. 구현모습

1) 메인 페이지

2) 상세 페이지

2. 시나리오

  1) 메인페이지에서 input창에 내용 입력하여 버튼 클릭 시  dispatch를 통해 setTodo에 새로운 데이터를 payload로 전송
     → 전송된 데이터를 useSelector를 통해 가져온 데이터 리스트를 화면에 뿌림


  2) 체크버튼 클릭 시 상태 변경 : 완료↔진행중, dispatch를 통해 isDone 상태 변경내용 전달


  3) Todo 제목 클릭 시 상세 페이지로 이동
  4) 상세페이지에서는 id, title, contents를 보여줌
  5) 수정 버튼 클릭 시 제목, 내용 수정 화면이 나타나고 버튼명은 완료로 변경됨


  6) 삭제버튼 클릭 시 해당 todo는 삭제되고 navigate를 통해 메인페이지로 넘어감
  7) Home 버튼 클릭시 메인 페이지로 넘어감
  8) 리덕스는 다음과 같이 설정

 

3. 파일구조

src
ㄴcomponents
    ㄴ button.js
    ㄴ form.js
    ㄴ header.js
    ㄴ todo.js
ㄴpages
    ㄴContents.jsx
    ㄴTodo.jsx
ㄴredux
    ㄴ config
        ㄴconfigStore.js
    ㄴ modules
         ㄴtodo.js
ㄴshared
    ㄴ Router.js
App.jsx

 

4. 수정필요한 부분

1) 상세페이지에서 input창을 클릭 안해도 이벤트가 발생하여 수정 완료 버튼을 누르게 되면 내용이 사라짐
2) 메인페이지에서 내용 입력 후 Enter키 눌렀을 때 alert창 뜨는 오류

 

5. 회고

리뷰 시간에 다른 분들의 코드도 보게 되었는데 다른 분들의 코드는 굉장히 깔끔했다.
페이지 분리도 정말 필요한 코드만 정리된 느낌이었는데 내 코드를 보니 왜이렇게 길지? 생각이 들었다.
음, 다른 분들은 수정 기능이 없어서? 아니면 내가 수정, 삭제 기능을 두번째 페이지에 적용했기 때문에 첫페이지에서 내용을 받아오고 두번째 페이지에서 또 그걸 수정하도록 구현해야하니까 내 코드가 더 길어보이는걸까?
클린 코드에 대해서 공부해볼 시간이 필요할 것 같다.
또 아직 스타일드 컴포넌트가 익숙하지 않아서 적용해보는 것에만 의의를 두었는데
그 부분도 css 파일이 따로 있으면서 추가로 구현했기 때문에 더 지저분해 보인 것 같다.

그래도 이번 과제를 돌이켜 칭찬해보자면
1. 클론 코딩이 되지 않도록 나만의 기능을 추가해본 점
  1) 메인화면에서 버튼이 아닌 체크박스를 통해 상태 변경을 진행한 점
  2) 수정 기능을 추가 한 점
  3) 메인 페이지를 깔끔하게 구현하기 위해 삭제를 상세페이지에서 가능하도록 구현한 점
2. 그러면서 발생했던 오류들을 해결해본 점
  1) 체크박스를 누르게 되면 checked = true인 상태로 변경이 되는데도 불구하고 화면에서는 체크가 해제된 것으로 보임
   → WHY?? 해답을 찾지 못했지만 추측해보자면 dispatch를 통해 isDone의 상태를 변경하고 그 상태로 렌더링만 하기 때문에 checked = true의 값을 받아오지 못하는 것은 아닐까..? 라고 살며시 생각해봤지만 아직 해답은 찾지 못했다.
  그래서 내가 생각한 대안은 'isDone이 true이면 defaultcheck를 true로 해주자.' 였다.
  임시방편일 수도 있지만 나름 대처를 잘했다고 생각한다.
3. 모르는 부분은 구글링을 통해 접하며 삽질해본 점

이미지 외에도 다양한 키워드를 검색해 코드에 적용해보거나 힌트를 얻을 수 있었다.

아직 해결하지 못한 부분 -> 질문해보기!!
1. 체크박스가 checked=true임에도 체크가 해제되어 있는 이유
2. 수정 시 input박스에 이벤트가 발생하지 않았음에도 수정이 되는 이유
3. 엔터키를 눌렀을 때 한번씩 씹히는 이유?

 

6. 코드

깃허브:https://github.com/jeLee94/TodoList/tree/version2-redux

 

GitHub - jeLee94/TodoList

Contribute to jeLee94/TodoList development by creating an account on GitHub.

github.com

App.jsx

import React from 'react';
import Router from './shared/Router';
import './App.css';

function App() {
  return <Router />;
}

export default App;

 

TodoPage.jsx // 메인페이지 Todo 추가, 완료상태 변경 코드

/* eslint-disable array-callback-return */
import { useDispatch, useSelector } from 'react-redux';
import { useState } from 'react';
import uuid from 'react-uuid';
import { setTodo, updateTodo } from '../redux/modules/todo';
import Form from '../components/form/form';
import Todo from '../components/todo/Todo';
import Header from '../components/header/header';

const TodoPage = () => {
  const dispatch = useDispatch();
  let { todos } = useSelector((state) => state.todo);
  const [title, setTitle] = useState('');

  // input에 내용 입력시 제목 셋팅하는 함수
  const onSetTodoHandler = (event) => {
    const { value } = event.target;
    if (event.target.name === 'title') setTitle(value); //제목설정
  };

  // todo 추가 버튼 클릭시 호출되는 함수
  const onSubmitTodoHandler = () => {
    if (title === '') {
      alert('내용을 추가해주세요.');
    } else {
      setTitle(''); //저장되어있는 제목, 내용 초기화
      dispatch(
        setTodo({
          id: uuid(),
          title,
          contents: '',
          isDone: false,
        })
      );
    }
  };

  // 체크박스로 Todo 상태 변경함수
  const onChangeHandler = ({ e, todo }) => {
    todo.isDone = !todo.isDone;
    // /todos = todos.filter((t) => t !== todo);

    dispatch(updateTodo());
  };

  return (
    <div>
      <Header />
      <Form
        submitHandler={onSubmitTodoHandler}
        inputHandler={onSetTodoHandler}
      />

      <h2>Working</h2>
      {todos.map((todo) => {
        if (!todo.isDone)
          //false
          return (
            <Todo
              todo={todo}
              key={todo.id}
              changeHandler={onChangeHandler}
              name='isNotDone'
            />
          );
      })}
      
      <h2>Done</h2>
      {todos.map((todo) => {
        if (todo.isDone)
          return (
            <Todo
              todo={todo}
              key={todo.id}
              changeHandler={onChangeHandler}
              name='isDone'
            />
          );
      })}
    </div>
  );
};
export default TodoPage;

 

Contents.jsx //상세 페이지, 스타일드 컴포넌트 적용 Todo 제목, 내용 수정 및 삭제 함수

import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { deleteTodo, updateTodo } from '../redux/modules/todo';
import { useState } from 'react';
import CustomBtn from '../components/button/button';
import styled from 'styled-components';

const Contents = () => {
  const param = useParams();
  const dispatch = useDispatch();
  const navigation = useNavigate();
  let { todos } = useSelector((state) => state.todo);
  const todo = todos.find((todo) => todo.id === param.id);
  const [modify, setModify] = useState(false); //수정 완료 버튼 변경시 사용
  const [title, setTitle] = useState('');
  const [contents, setContents] = useState('');
  
  // input 입력시 제목,내용 셋팅
  //Todo1: input창 클릭 안하고 완료하면 내용 사라지는 문제
  //Todo2: 제목만 수정시(내용은 클릭만했을때) 화면 순서 달라지는 문제
  const onSetTodoHandler = (event) => {
    const { value } = event.target;
    if (event.target.name === 'title') setTitle(value); //제목설정
    else if (event.target.name === 'contents') {
      setContents(value); //내용설정
    }
  };
  
  //삭제 함수
  const onDeleteHandler = () => {
    dispatch(deleteTodo(param));
    navigation('/');
  };

  // 상태 변경함수
  const onModifyHandler = (child) => {
    setModify(!modify);
    console.log(child);
    console.log(title, contents);
    if (child === '완료') {
      dispatch(updateTodo({ id: param.id, title, contents }));
      todo.contents = contents;
      todo.title = title;
    }
  };

  return (
    <StContainer>
      <StDialog>
        <div>
          <StDialogHeader>
            <StButton onClick={() => navigation('/')}>HOME</StButton>
            <p>id: {todo.id.substr(0, 4)}</p>
          </StDialogHeader>
        </div>
        {!modify && ( //modify가 false이면 제목과 내용 보여주기
          <div>
            <StTitle>{todo.title}</StTitle>
            <StBody>{todo.contents}</StBody>
          </div>
        )}

        {modify && (
          //modify가 true이면 input창 보여주기
          <div>
            <p>
              <input
                name='title'
                type='text'
                defaultValue={todo.title}
                onBlur={(e) => {
                  e.preventDefault();
                  onSetTodoHandler(e);
                }}
              />
            </p>
            <p>
              <textarea
                name='contents'
                defaultValue={todo.contents}
                onBlur={(e) => {
                  e.preventDefault();
                  onSetTodoHandler(e);
                }}
              ></textarea>
            </p>
          </div>
        )}
        <div>
          <CustomBtn onClick={onModifyHandler}>
            {modify ? '완료' : '수정'}
          </CustomBtn>
          <CustomBtn onClick={onDeleteHandler}>삭제</CustomBtn>
        </div>
      </StDialog>
    </StContainer>
  );
};

export default Contents;

const StContainer = styled.div`
  border: 2px solid #eee;
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
`;

const StDialog = styled.div`
  width: 600px;
  height: 400px;
  border: 1px solid #345e44;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding-bottom: 30px;
`;

const StDialogHeader = styled.div`
  display: flex;
  height: 80px;
  width: 553px;
  justify-content: space-between;
  padding: 0 24px;
  align-items: center;
  border-radius: 10px;
  background-color: #bbddc8;
`;

const StTitle = styled.h1`
  padding: 0 24px;
`;

const StBody = styled.main`
  padding: 0 24px;
`;

const StButton = styled.button`
  border: 1px solid ${({ borderColor }) => borderColor};
  height: 40px;
  width: 120px;
  background-color: #345e44;
  color: #bbddc8;
  border-radius: 12px;
  cursor: pointer;
`;

 

Router.js

import React from 'react';
// 1. react-router-dom을 사용하기 위해서 아래 API들을 import 합니다.
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Contents from '../pages/Contents';
import TodoPage from '../pages/TodoPage';

// 2. Router 라는 함수를 만들고 아래와 같이 작성합니다.
//BrowserRouter를 Router로 감싸는 이유는,
//SPA의 장점인 브라우저가 깜빡이지 않고 다른 페이지로 이동할 수 있게 만들어줍니다!
const Router = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path='/' element={<TodoPage />} />
        <Route path='contents/:id' element={<Contents />} />
      </Routes>
    </BrowserRouter>
  );
};

export default Router;

 

todo.js //리덕스 설정

import uuid from 'react-uuid';
//Action Value
const SET_TODO = 'SET_TODO';
const UPDATE_TODO = 'UPDATE_TODO';
const DELETE_TODO = 'DELETE_TODO';

//Action Creator
export const setTodo = (payload) => {
  return {
    type: SET_TODO,
    payload,
  };
};

export const updateTodo = (payload) => {
  return {
    type: UPDATE_TODO,
    payload,
  };
};

export const deleteTodo = (payload) => {
  return {
    type: DELETE_TODO,
    payload,
  };
};

//Initial State
const initialState = {
  todos: [
    {
      id: uuid(),
      title: '리액트 투두1',
      contents: '리액트를 공부해봅시다1',
      isDone: false,
    },
    {
      id: uuid(),
      title: '리액트 투두2',
      contents: '리액트를 공부해봅시다2',
      isDone: true,
    },
  ],
};

//Reducer
const todos = (state = initialState, action) => {
  switch (action.type) {
    case SET_TODO: {
      console.log(state, action.payload);
      return {
        ...state,
        todos: [...state.todos, action.payload],
      };
    }
    case UPDATE_TODO: {
      return {
        ...state,
        todos: [...state.todos],
      };
    }
    case DELETE_TODO: {
      return {
        todos: state.todos.filter((todo) => todo.id !== action.payload.id),
      };
    }
    default:
      return state;
  }
};
//export default reducer

export default todos;

 

------컴포넌트-----

버튼 컴포넌트

import React from 'react';
import './style.css';

export default function CustomBtn(props) {
  const { onClick, children } = props;

  return (
    <button className='CustomBtn' onClick={() => onClick(children)}>
      {children}
    </button>
  );
}

 

form 컴포넌트

import React from 'react';
import './style.css';

export default function Form(props) {
  const { submitHandler, inputHandler } = props;
  return (
    <div>
      <form
        onKeyDown={(e) => {
          console.log(e);
          if (e.key === 'Enter') {
            e.preventDefault();
            submitHandler();
            // e.target[1].value = ''; //input창 초기화
          }
        }}
        onSubmit={(e) => {
          e.preventDefault();
          submitHandler(e);
          e.target[0].value = ''; //input창 초기화
          // e.target[1].value = ''; //input창 초기화
        }}
      >
        <p className='Input_area'>
          <label>TODO </label>
          <input
            type='text'
            name='title'
            onBlur={(e) => {
              inputHandler(e);
            }}
          />

          {/* <label> 내용 </label>
          <input type='text' name='contents' onBlur={(e) => inputHandler(e)} /> */}
        </p>
        <p className='Btn'>
          <input type='submit' value='추가' />
        </p>
      </form>
    </div>
  );
}

 

Todo 내용 컴포넌트

import React from 'react';
import { Link } from 'react-router-dom';

// import CustomBtn from '../button/button';
import './style.css';

export default function Todo(props) {
  const { todo, name, changeHandler } = props;
  // console.log(todo.id);
  //체크박스가 event.target.checked가 true임에도 해제되어있어서 임의로 name이 isDone이면 defaultChecked되도록 구현함
  return (
    <div className='Box'>
      <p>
        {name === 'isDone' ? (
          <input
            type='checkbox'
            name={name}
            onClick={(e) => {
              changeHandler({ e, todo });
            }}
            defaultChecked
          />
        ) : (
          <input
            type='checkbox'
            name={name}
            onClick={(e) => {
              changeHandler({ e, todo });
            }}
          />
        )}
        {/*Path Varialble 사용*/}
        <Link to={`/contents/${todo.id}`}>{todo.title}</Link>
      </p>
    </div>
  );
}

 

반응형

댓글