본문 바로가기

프로젝트 회고 🌊

React 리액트 투두리스트 앱 (+ Redux, Styled-component)

💻 Project name: Todo-List

🕚 Project Execution Period: 2023.06.27 ~ 2023.06.29

 

항해99에서 주특기 리액트 2주차 과제로 다른 팀원 한명과 간단한 투두리스트 앱을 만들었다. 과제에서 요구된 사항은 할 일 추가 / 조회/ 삭제 / 수정 기능이었고, 추가적으로는 할 일 상세 내용 업데이트와 반응형 웹을 구현했고 사이드바에서는 오늘 날짜를 띄웠다.

 

배포: https://todolist-react-jade.vercel.app/    (vercel)

https://github.com/nayoung3669/todolist_react

 

 

GitHub - nayoung3669/todolist_react: 리액트 리덕스 투두 앱 ✍🏻 (페어)

리액트 리덕스 투두 앱 ✍🏻 (페어). Contribute to nayoung3669/todolist_react development by creating an account on GitHub.

github.com

 

 

1. 주요 기능

1) 데스크탑 버전

 

 

2) 모바일 버전

 

 

1주차 과제 요구사항으로도 구현 가능했지만 2주차에 배운 내용이 redux, styled-component, routing 등이었기 때문에 최대한 활용하려고 노력했다.

 

그리고 이번 과제에서는 과제시작 전 초반에 코드 컨벤션과 디렉토리 구조, 그리고 컴포넌트를 나누는 것에도 중점을 두었다. 자바스크립트 언어를 사용하는 리액트 구조 상 변수나 함수등은 최대한 작은 지역에서 선언되는 것이 좋고, 컴포넌트와 함수 등은 기능별로 나누어져 재사용하는것이 중요하다.

 

 

2. Directory Structure

 

 

무엇보다 컨테이너 컴포넌트와 프레젠테이셔널 컴포넌트를 나누었다.그리고 뷰를 담당하는 컴포넌트에서는 훅을 사용하거나 리덕스에 접근하지 못하게 하고, 컨테이너 컴포넌트에서는 리덕스 스토어에 접근할 수 있도록 하고 그 값을 프레젠테이셔널 컴포넌트에 props로 전달하여 간접적으로 사용할 수 있게 해 UI와 Data를 분리했다.

 

 

이렇게 하니 나중에 미디어쿼리로 모바일 사이즈 만들 때 재사용하기 편리했다. 

 

 

 

 

3. 라우팅

import { Route, Routes } from "react-router-dom";
import DetailPage from "../pages/DetailPage";
import MainPage from "../pages/MainPage";

const Router = () => {
  return (
    <Routes>
      <Route path="/" element={<MainPage />} />
      <Route path=":id" element={<DetailPage />} />
    </Routes>
  );
};

export default Router;

투두 아이템에 onClick 핸들러를 선언하고 이벤트가 발생하면 navigate(":id")를 호출해 디테일페이지로 이동

 

//App.js

function App() {
  return (
    <div className="App">
      <GlobalStyle />
      <Router />
    </div>
  );
}

App에는 GlobalStyle과 Router을 렌더링한다.

 

 

4. 리덕스

Ducks 패턴을 이용해 todos에 관련된 액션 변수, 액션 생성 함수, 투두 초깃값, 리듀서를 모두 modules 안의 한 파일에 몰아넣었다.

 

const EDIT = "todos/EDIT";
const CHANGE_INPUT = "todos/CHANGE_INPUT";
const ADD = "todos/ADD";
const TOGGLE = "todos/TOGGLE";
const REMOVE = "todos/REMOVE";

export const edit = (input) => ({
  type: EDIT,
  input,
});

export const changeInput = (input) => ({ type: CHANGE_INPUT, input });
let nextId = 6;
export const add = (todo) => {
  return {
    type: ADD,
    todo: {
      id: nextId++,
      title: todo.title,
      text: todo.text,
      done: false,
    },
  };
};
export const toggle = (id) => ({ type: TOGGLE, id });
export const remove = (id) => ({ type: REMOVE, id });

changeInput 함수는 객체 형태의 input 을 매개변수로 받는다. 그래서

 

const TodoInputContainer = () => {
  const dispatch = useDispatch();
  const input = useSelector(({ todos }) => todos.input);

  const onChangeInput = (e) => {
    const { name, value } = e.target;
    const newInput = {
      ...input,
      [name]: value,
    };
    dispatch(changeInput(newInput));
  };
  
  //...
  
  }

각각 e.target.name과 value를 할당한 뒤 값을 관리해주어야한다. 이 부분이 조금 어려워서 다른 조 팀원과 상의했지만, 사실 리덕스로 바뀌었을 뿐 useState에서 사용되는 onChange와 크게 다를바가 없었다.

 

 

이 컨테이너에서 데이터를 조회하고, 바뀐 값을 prop로 전달하면 컨테이너 안의 Input 컴포넌트가 받아서 value={}에 각각 저장한다. 그리고 전달받은 onChange함수를 실행한다.

 

const TodoInput = ({ title, text, onChange }) => {
  return (
    <TodoInputBlock>
      <p>할 일:</p>
      <input type="text" name="title" value={title} onChange={onChange} />
      <p>설명: </p>
      <input type="text" name="text" value={text} onChange={onChange} />
    </TodoInputBlock>
  );
};

 

 

5. layout

어떤 크기의 창이든 기반이 되는 레이아웃은 TodoTemplate.js에서 선언하고 스타일링해준다. 그리고 디테일페이지에서도 이 템플릿을 재사용한다.

 

const TodoTemplate = ({ children }) => {
  const { id } = useParams();

  return (
    <TemplateBlock>
      {isNaN(id) && <Sidebar />}
      <div className="todoTitle">
        <p>투두 리스트</p>
      </div>
      <div className="content">{children}</div>
    </TemplateBlock>
  );
};

export default TodoTemplate;

 

params를 가져오는 이유는 이 페이지가 홈페이지인지, 디테일 페이지인지 확인하기 위함이다. 디테일페이지이면 sidebar을 숨겨야하기 때문이다. 그리고 todoTitle 즉, header를 제외한 나머지는 {children} props로 받아서 보여준다. 

 

 

 

const DetailPage = () => {
  const { id } = useParams();

  return (
    <div>
      <TodoTemplate>
        <TodoItemDetail id={id} />
      </TodoTemplate>
    </div>
  );
};

 

디테일페이지에서는 이렇게 import해와서 사용하고 있음!

 

 

6. assets / styles

 

생각보다 아이콘이 많아져서 icons.js 파일을 만들고 각각 export 해줬다.

 

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";

export const trashIcon = () => {
  return <FontAwesomeIcon icon={faTrash} />;
};

export const doneIcon = () => {
  return <FontAwesomeIcon icon={faCircleCheck} style={{ color: "#4e7dd0" }} />;
};

export const backIcon = () => {
  return (
    <FontAwesomeIcon
      icon={faHourglass}
      size="md"
      style={{ color: "#4e7dd0" }}
    />
  );
};


// ...
// ...

 

 

 

리액트 첫 팀 프로젝트였는데 아직까지는 수월했지만 프로젝트가 커질수록 신경써야하는 부분이 기하급수적으로 늘어날 것만 같다 .... 그리고 항해99 전시회에 있는 우수작품들 깃헙을 보면서 코드 구조가, 컴포넌트 나눈 부분, 역할 분담, 스타일 관리, 리덕스 데이터 관리 등을 많이 참고해야겠다고 느꼈다. 시간을 내서 프로젝트 몇개를 깊게 파봐야겠다