포스트

2024.06.20 프로그래머스 - React로 만드는 간단 ToDo 1

React로 ToDoList 만들어보기 1

클래스형 컴포넌트와 함수형 컴포넌트

클래스형 컴포넌트

  • 리액트 초기에 사용했던 컴포넌트방식
  • 생명주기 메서드가 있어 다양한 단계에서의 렌더링 조작이 가능하다.

  • 간단한 클래스 컴포넌트 작성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
      // ClassComponent.tsx
      import { Component, ReactNode } from "react";
        
      class ClassComponent extends Component {
      	// 생명주기, state 관련 로직 작성
        
        render(): ReactNode {
          // Class Component의 출력부
          return (
            <div className="container">
              <h1>HEllo World</h1>
              <p>이것은 클래스 컴포넌트</p>
            </div>
          );
        }
      }
        
      // 외부 import를 위한 export
      export default ClassComponent;
        
    
    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
    
      import React from "react";
      import logo from "./logo.svg";
      import "./App.css";
        
      // Class Component 호출
      import ClassComponent from "./components/ClassComponent";
        
      function App() {
        const date = new Intl.DateTimeFormat("ko");
        
        return (
          <>
            <div className="App-header">
              <h1>Hello React</h1>
              <p>반가우이</p>
              <p>오늘 날짜 : {date.format()}</p>
            </div>
            {/* ClassComponent 사용 */}
            <ClassComponent />
          </>
        );
      }
        
      export default App;
        
    
    • Class Component는 호출하고 사용하는 과정이 함수 컴포넌트와 다르지 않다.

함수형 컴포넌트

  • 현재 권장되는 방식의 컴포넌트
  • 사용이 간편하다
  • import 및 export 부분은 클래스형 컴포넌트와 동일하게 진행

  • 간단한 함수형 컴포넌트 작성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      // FuncComponent.tsx
      import React from "react";
        
      function FuncComponent() {
        return <div>함수형 컴포넌트</div>;
      }
        
      export default FuncComponent;
        
    
    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
    
      // App.tsx
      import React from "react";
      import logo from "./logo.svg";
      import "./App.css";
      import FuncComponent from "./components/FuncComponent";
        
      function App() {
        const date = new Intl.DateTimeFormat("ko");
        
        return (
          <>
            <div className="App-header">
              <h1>Hello React</h1>
              <p>반가우이</p>
              <p>오늘 날짜 : {date.format()}</p>
            </div>
        			
      			{/*함수형 컴포넌트 사용*/}
            <FuncComponent />
          </>
        );
      }
        
      export default App;
        
    

state 사용해보기

  • React 의 useState훅을 사용하여, 컴포넌트 내부의 상태 를 수정 및 사용할 수 있다.

    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
    
      import React, { useState } from "react";
        
      function TodoList(): JSX.Element {
        const [todos, setTodos] = useState<string[]>([
          "공부하기",
          "잠자기",
          "밥먹기",
        ]);
        const title = "오늘 할 일";
        return (
          <div className="container">
            <h1>{title}</h1>
            <div className="container">
              <div className="board">
                <ul>
                  {todos.map((todo) => (
                    <li>{todo}</li>
                  ))}
                </ul>
              </div>
            </div>
          </div>
        );
      }
        
      export default TodoList;
        
    
  • useState훅은 배열로 state 와 state Setter 를 반환한다.
  • state Setter 로 state에 대한 값을 변경(수정) 할 수 있다.

데이터 반복 처리

  • 보통 열거형 데이터의 반복 처리는 map 메소드를사용하여 처리한다.

    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
    
      import React, { useState } from "react";
      import TodoListItem from "./TodoListItem";
        
      type TodoType = {
        id: number;
        text: string;
        isChecked: boolean;
      };
        
      const initialTodos: TodoType[] = [
        {
          id: 1,
          text: "공부하기",
          isChecked: false,
        },
        {
          id: 2,
          text: "잠자기",
          isChecked: false,
        },
        {
          id: 3,
          text: "밥먹기",
          isChecked: false,
        },
      ];
        
      function TodoListContainer(): JSX.Element {
        const [todos, setTodos] = useState<TodoType[]>(initialTodos);
        const title = "오늘 할 일";
        return (
          <div className="container">
            <h1>{title}</h1>
            <div className="container">
              <div className="board">
                <ul className="todo-list">
                  {todos.map((todo) => (
                    <TodoListItem key={todo.id} text={todo.text} />
                  ))}
                </ul>
              </div>
            </div>
          </div>
        );
      }
        
      export default TodoListContainer;
        
    
    • map 또한 축약형으로 생략되었지만, return(반환문) 이 적용되어있다.
  • 열거형 데이터 (반복가능한)를 React Component로 다룰때는 key 속성을 유의하여 작성해야 한다.

key

  • React의 key는 해당 항목에 대한 식별자 역할을하며, 엘리먼트에 대한 고유성을 부여한다.
  • React는 key를 활용하여, 변경이 필요한 부분만을 변경 및 엽데이트를 수행할 수 있기 때문에, 배열형 엘리먼트를 렌더링 할 때 특히 신경써주어야 하는 부분이다.

체크 박스 기능 추가

  • 위의 todo 어플리케이션에 투두 목록을 체크 하면, 삭선이 그어지도록 코드를 추가하였다.

    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
    
      // TodoListItem.tsx
        
      import React from "react";
        
      type TodoListItemType = {
        text: string;
        todoId: number;
        onChecked: (todoId: number) => void;
        isChecked: boolean;
      };
        
      function TodoListItem({
        todoId,
        text,
        onChecked,
        isChecked,
      }: TodoListItemType) {
        return (
          <li className="todo-list-item">
            <input type="checkbox" className="" onChange={() => onChecked(todoId)} />
            {!isChecked && <span>{text}</span>}
            {isChecked && <del>{text}</del>}
          </li>
        );
      }
        
      export default TodoListItem;
        
    
    • check 엘리먼트를 누르면 props로 받은 onChange 콜백이 실행되어 isChecked Props의 값이 변경 됨에 따라 렌덩히라는 결과물이 달라진다.
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
// TodoListContainer.tsx

 import React, { useState } from "react";
import TodoListItem from "./TodoListItem";
import { setDefaultResultOrder } from "dns";

type TodoType = {
  id: number;
  text: string;
  isChecked: boolean;
};

const initialTodos: TodoType[] = [
  {
    id: 1,
    text: "공부하기",
    isChecked: false,
  },
  {
    id: 2,
    text: "잠자기",
    isChecked: false,
  },
  {
    id: 3,
    text: "밥먹기",
    isChecked: false,
  },
];

function TodoListContainer(): JSX.Element {
  const [todos, setTodos] = useState<TodoType[]>(initialTodos);

	// isChecked 핸들러
  function checkHandler(todoId: number) {
    setTodos((prevTodos) =>
      prevTodos.map((item) =>
        item.id === todoId ? { ...item, isChecked: !item.isChecked } : item
      )
    );
  }

  const title = "오늘 할 일";
  return (
    <div className="container">
      <div className="container">
        <div className="board">
          <ul className="todo-list">
            {todos.map((todo) => (
              <TodoListItem
                key={todo.id}
                todoId={todo.id}
                text={todo.text}
                onChecked={checkHandler}
                isChecked={todo.isChecked}
              />
            ))}
          </ul>
        </div>
      </div>
    </div>
  );
}

export default TodoListContainer;

할 일 생성하기

  • React Bootstrap 의 Button 컴포넌트사용

Bootstrap Button 컴포넌트 사용 에러

1
2
3
'Button' cannot be used as a JSX component.
  Its return type 'ReactNode' is not a valid JSX element.
    Type 'undefined' is not assignable to type 'Element | null'.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";
import { Button } from "react-bootstrap";

function TodoGenerator(): JSX.Element {
  return (
    <div className="todo-generator">
      <input type="text" />
      <Button variant="primary">생성</Button>
    </div>
  );
}

export default TodoGenerator;

  • 위의 코드로 TodoGenerator를 구성했을 때 에러가 발생하였다.

여러가지 검색을 통해 다양한 케이스를 보고 설정 파일(package.json, tsconfig 등) 을 수정해보기도 하고

타입 명시나 CustomComponent형식으로 변경하여 해보았으나

해결되지 않았다….

일단 CRA로 React를 만들었기 떄문에 Vite로 재구축 하여 해당 과정을 수행해보니

ReactBootStrap이 아주 잘 되었다…

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
// App.tsx
import { useState } from "react";
import "./App.css";
import TodoListContainer from "./components/TodoListContainer";
import TodoGenerator from "./components/TodoGenerator";
import Clock from "./components/Clock";

type TodoType = {
  id: number;
  text: string;
  isChecked: boolean;
};

const initialTodos: TodoType[] = [
  {
    id: 1,
    text: "공부하기",
    isChecked: false,
  },
  {
    id: 2,
    text: "잠자기",
    isChecked: false,
  },
  {
    id: 3,
    text: "밥먹기",
    isChecked: false,
  },
];

function App() {
  const [todos, setTodos] = useState<TodoType[]>(initialTodos);

  function checkHandler(todoId: number) {
    setTodos((prevTodos) =>
      prevTodos.map((item) =>
        item.id === todoId ? { ...item, isChecked: !item.isChecked } : item
      )
    );
  }

  function addTodoItem(text: string) {
    setTodos((prevTodos) => [
      ...prevTodos,
      { id: prevTodos.length + 1, text: text, isChecked: false },
    ]);
  }

  const title = "오늘 할 일";

  return (
    <>
      <div className="container">
        <h1>{title}</h1>
        <TodoGenerator addTodoHandler={addTodoItem} />
        <Clock />
        <TodoListContainer todos={todos} checkHandlerFunc={checkHandler} />
      </div>
    </>
  );
}

export default App;

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
// TodoGenerator.tsx

import { useState, ChangeEvent } from "react";
import { Button } from "react-bootstrap";

type TodoGeneratorProps = {
  addTodoHandler: (text: string) => void;
};

function TodoGenerator({ addTodoHandler }: TodoGeneratorProps): JSX.Element {
  const [newTodo, setNewTodo] = useState<string>("");

  function changeTodoHandler(e: ChangeEvent<HTMLInputElement>) {
    setNewTodo(e.target.value);
  }

  return (
    <div className="todo-generator">
      <input type="text" onChange={(e) => changeTodoHandler(e)} />
      <Button
        as="button"
        variant="primary"
        onClick={() => addTodoHandler(newTodo)}
      >
        생성
      </Button>
    </div>
  );
}

export default TodoGenerator;

현재 시간 컴포넌트

  • 현재 시간을 알려주는 컴포넌트를 TodoGenerator 밑에 추가 한다.
  • new Date()를 1초마다 할당한 state를 렌더링!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      import { useState } from "react";
        
      export default function Clock(): JSX.Element {
        const [time, setTime] = useState(new Date());
        
        setInterval(() => {
          setTime(new Date());
        }, 1000);
        
        return (
          <div>
            <h2>현재 시간 - {time.toLocaleTimeString()}</h2>
          </div>
        );
      }
    

할 일 삭제하기

  • 생성한 할일 목록을 지우려면, state로 관리중인 todos의 값 자체를 변경해야 한다.
  • App.tsx 컴포넌트에서 todos에 대한 변경 함수를 작성후, Prop 내려주기를 통해 TodoListItem 에 함수를 전달해주면 해당 item에 대한 삭제 작업이 가능하다.
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
// App.tsx
import { useState } from "react";
import "./App.css";
import TodoListContainer from "./components/TodoListContainer";
import TodoGenerator from "./components/TodoGenerator";
import Clock from "./components/Clock";

type TodoType = {
  id: number;
  text: string;
  isChecked: boolean;
};

const initialTodos: TodoType[] = [
  {
    id: 1,
    text: "공부하기",
    isChecked: false,
  },
  {
    id: 2,
    text: "잠자기",
    isChecked: false,
  },
  {
    id: 3,
    text: "밥먹기",
    isChecked: false,
  },
];

function App() {
  const [todos, setTodos] = useState<TodoType[]>(initialTodos);

  function checkHandler(todoId: number) {
    setTodos((prevTodos) =>
      prevTodos.map((item) =>
        item.id === todoId ? { ...item, isChecked: !item.isChecked } : item
      )
    );
  }

  function addTodoItem(text: string) {
    setTodos((prevTodos) => [
      ...prevTodos,
      { id: Math.random(), text: text, isChecked: false },
    ]);
  }

// 할 일 삭제
  function removeTodoItem(todoId: number) {
    setTodos((prevTodos) => {
      const filteredTodos = prevTodos.filter((todo) => todo.id !== todoId);
      return [...filteredTodos];
    });
  }

  const title = "오늘 할 일";

  return (
    <>
      <div className="container">
        <h1>{title}</h1>
        <TodoGenerator addTodoHandler={addTodoItem} />
        <Clock />
        <TodoListContainer
          todos={todos}
          checkHandlerFunc={checkHandler}
          removeTodo={removeTodoItem}
        />
      </div>
    </>
  );
}

export default App;

  • removeTodoItem 함수를 작성하여 TodoListContainer에 props로 전달하였다.
    • filter 메소드를 이용하여 기존에 가지고 있던 todos에서 인수로 전달받은 id 요소만을 제외한 나머지 todos를 다시 할당해주는 방식으로 기존 todo를 유지하였다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.