Post

컴포넌트 분리한 todo 앱 만들기

결과물

absolute

파일 구성

  • components 폴더 생성
  • components/Controller.js, viewers.js 파일 생성

코드 작성

App.js

  • App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import "./App.css";
// (1) useReducer과 useRef를 가져오도록한다
import { useReducer, useRef } from "react";
import Header from "./components/Header";
import TodoEditor from "./components/TodoEditor";
import TodoList from "./components/TodoList";

// (2) TodoList 컴포넌트에서 사용할 배열 데이터로 useReducer로 관리 예정이다
const mockTodo = [
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    createdDate: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "노래 연습하기",
    createdDate: new Date().getTime(),
  },
];

// (3) useReducer를 사용하여 자식 컴포넌트에서 데이터의 CRUD 요청이왔을때 처리하기 위한 함수
function reducer(state, action) {
  // 상태 변화 코드
  switch (action.type) {
    // (4) CREATE 요청이 오면 action 객체에 들어있는 newItem속성의 값을 복사하고
    // 기존의 todo 데이터(useRef로 등록한 mockTodo데이터)와 합쳐서 todo 데이터로 리턴한다
    case "CREATE": {
      return [action.newItem, ...state];
    }
    // (5) 이 앱에서의 UPDATE 요청은 할일완료/미완료 체크박스 선택으로, 이벤트 발생시 해당 아이템을 찾기위해
    // state인 todo 데이터를 map을 돌려서 id값이 업데이트 요청온 action.targetId와 비교후 맞다면
    // 스프레드 연산자를 통한 복사를 통해 isDone의 값만 반대로 변경해주고 아니라면 기존 데이터 그대로를 리턴한다
    case "UPDATE": {
      return state.map((it) =>
        it.id === action.targetId ? { ...it, isDone: !it.isDone } : it
      );
    }
    // (6) Delete 요청이 오면 filter키워드를 사용해서 해당 id가 아닌 데이터들만 따로 필터링하여 리턴해준다
    case "DELETE": {
      return state.filter((it) => it.id !== action.targetId);
    }
    default:
      return state;
  }
  return state;
}

function App() {
  // 위에서 생성한 mockTodo 배열 데이터를 초기값으로 갖는 todo라는 데이터 변수를 만들고 useReducer로 관리하도록 선언한다
  // dispatch를 호출하면 위에서 미리 생성해둔 reducer함수를 호출하며 인자값을 전달하고, reducer함수에서 리턴한 값은 todo 변수에 반영하고 컴포넌트 재렌더링을 진행한다.
  const [todo, dispatch] = useReducer(reducer, mockTodo);

  // 삭제, 업데이트 등의 데이터 변경을 해주기 위해서는 해당 데이터들마다 구분할 수 있는 중복되지 않는 특별한 id값을 가지고 있어야하는데
  // 이 앱에서는 간단하게 변수의 데이터가 변하더라도 재렌더링이 되지 않는 useRef를 사용하여 idRef변수를 생성후 id값을 관리하도록 한다
  // 초기값은 기존의 데이터가 2개가 이미 들어가있기 때문에 3부터 시작하도록 주었다
  const idRef = useRef(3);

  // 데이터 생성을 하는 컴포넌트는 TodoEditor 컴포넌트로 해당 자식컴포넌트가 데이터를 생성할 때 사용하기 위한 함수를 만들어서 제공해주려한다
  // onCreate함수로 선언하고 dispatch는 위에서 만들어둔 reducer를 통한 데이터 처리를 하기위해 호출하며
  // reducer함수에서 CREATE처리에 필요한 인자값인 type과 newItem 객체데이터를 생성하여 전달한다
  // id값의 idRef.current는 현재 idRef의 값을 불러올때 사용된다 content는 자식 컴포넌트에서 데이터를 생성하기위한 데이터를 전달하면 해당 값을 넣어준다
  // isDone은 완료/비완료 항목으로 기본은 false로 주도록한다
  // 생성날짜인 createdDate속성은 현재 시간을 넣어준다
  const onCreate = (content) => {
    dispatch({
      type: "CREATE",
      newItem: {
        id: idRef.current,
        content,
        isDone: false,
        createdDate: new Date().getTime(),
      },
    });
    idRef.current += 1;
  };

  // 데이터를 보여주는 역할을 담당하는 TodoList컴포넌트에서 사용할 onUpdate함수는 자식 컴포넌트인 TodoList로부터 업데이트할 데이터의 id값(targetId)를 전달받아서
  // reducer함수로 전달한다
  const onUpdate = (targetId) => {
    dispatch({ type: "UPDATE", targetId });
  };
  // 데이터를 보여주는 역할을 담당하는 TodoList컴포넌트에서 사용할 onDelete함수는 자식 컴포넌트인 TodoList로부터 삭제할 데이터의 id값(targetId)를 전달받아서
  // reducer함수로 전달한다
  const onDelete = (targetId) => {
    dispatch({ type: "DELETE", targetId });
  };

  return (
    <div className="App">
      <Header />
      <TodoEditor onCreate={onCreate} />
      <TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} />
    </div>
  );
}

export default App;

TodoEditor.js

  • /components/TodoEditor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import "./TodoEditor.css";
import { useState, useRef } from "react";
const TodoEditor = ({ onCreate }) => {
  const [content, setContent] = useState("");
  const inputRef = useRef();
  const onChangeContent = (e) => {
    setContent(e.target.value);
  };

  // 추가 버튼을 클릭시 동작하는 함수로 위에서 생성한 content변수가 비어있다면 useRef를 통해 텍스트 입력창으로 포커스를 넘긴다
  // alert 메세지 창 호출 함수를 사용해서 메세지를 띄워주도록한다
  // content가 비어있지 않다면 부모 컴포넌트한테 받은 onCreate함수로 content데이터를 전달한다, 이후 setContent 함수를 호출하여 content값을 빈값으로 변경해주도록했다
  const onSubmit = () => {
    if (!content) {
      inputRef.current.focus();
      alert("No Value");
      return;
    }
    onCreate(content);
    setContent("");
  };
  // 편의성 기능
  // 엔터 키코드는 13번 코드로 onKeyDown 이벤트가 발생하면 이벤트 키코드를 비교하여 13일 경우 onSubmit함수를 호출하도록하였다
  const onKeyDown = (e) => {
    if (e.keyCode === 13) {
      onSubmit();
    }
  };

  return (
    <div className="TodoEditor">
      <h4>새로운 Todo 작성하기 ✏️</h4>
      <div className="editor_wrapper">
        <input
          ref={inputRef}
          value={content}
          onChange={onChangeContent}
          onKeyDown={onKeyDown}
          placeholder="새로운 Todo ..."
        />
        <button onClick={onSubmit}>추가</button>
      </div>
    </div>
  );
};

export default TodoEditor;

TodoList.js

  • /components/TodoList.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import "./TodoList.css";
import TodoItem from "./TodoItem";
import { useState, useMemo } from "react";

const TodoList = ({ todo, onUpdate, onDelete }) => {
  const [search, setSearch] = useState("");

  const memoTodo = useMemo(() => {
    const totalCount = todo.length;
    const doneCount = todo.filter((it) => it.isDone).length;
    const notDoneCount = totalCount - doneCount;
    return { totalCount, doneCount, notDoneCount };
  }, [todo]);
  const onChangeSearch = (e) => {
    setSearch(e.target.value);
  };
  const getSearchResult = () => {
    return search === ""
      ? todo
      : todo.filter((it) => it.content.toLowerCase().includes(search));
  };
  const { totalCount, doneCount, notDoneCount } = memoTodo;
  return (
    <div className="TodoList">
      <h4>Todo List💫</h4>
      <div>
        <div>
          <div>총개수: {totalCount}</div>
          <div>완료된 할일: {doneCount}</div>
          <div>아직 완료하지 못한  : {notDoneCount}</div>
        </div>
      </div>
      <input
        onChange={onChangeSearch}
        className="searchbar"
        placeholder="검색어를 입력하세요"
      />
      <div className="list_wrapper">
        {getSearchResult().map((p) => (
          <TodoItem key={p.id} {...p} onUpdate={onUpdate} onDelete={onDelete} />
        ))}
      </div>
    </div>
  );
};
export default TodoList;

TodoList.js

  • /components/TodoList.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import "./TodoItem.css";
const TodoItem = ({ id, content, isDone, createDate, onUpdate, onDelete }) => {
  console.log(`${id} TodoItem 업데이트`);
  const onChangeCheckbox = () => {
    onUpdate(id);
  };
  const onClickDelete = () => {
    onDelete(id);
  };
  return (
    <div className="TodoItem">
      <div className="checkbox_col">
        <input checked={isDone} type="checkbox" onChange={onChangeCheckbox} />
      </div>
      <div className="title_col">{content}</div>
      <div className="date_col">
        {new Date(createDate).toLocaleDateString()}
      </div>
      <div onClick={onClickDelete} className="btn_col">
        <button>삭제</button>
      </div>
    </div>
  );
};

export default TodoItem;
This post is licensed under CC BY 4.0 by the author.