본문 바로가기

Programing/React

[React + SpringBoot] To-do List 만들기 - (1)(수정)

[React + SpringBoot] To-do List 만들기 - (1)

[React + SpringBoot] To-do List 만들기 - (2)

[React + SpringBoot] To-do List 만들기 - (3)

 

 

사용자 인터페이스를 만들기 위한 JavaScript 라이브러리 React!

 

일반적으로 Spring 과 React  이용하여 싱글 페이지 웹 애플리케이션을 개발하는 경우 크게 2가지 방법이 있다.

 

첫 번째는 하나의 스프링 프로젝트를 구성하고 해당 프로젝트 안에 Front-End 영역을 설정하는 것이다.

이렇게 하나의 Repository, IDE를 이용해서 개발을 진행하고 maven 등의 플러그인을 통해 빌드하기 때문에 한명의 개발자가 Full-Stack 으로 빠르게 개발할 때 편리하다.

 

두 번째는 Front-End 와 Back-End 의 프로젝트를 분리하고 각각 별도로 개발을 진행하는 방법이다.

이 경우 Front-End 와 Back-End 를 proxy 를 이용하여 연동하고 빌드는 개별적으로 진행하며, 프로젝트 규모가 커지고 개발자의 롤이 명확하다면 이와 같이 프로젝트를 분리하는 것이 유리하다.

 


 

1. 프로젝트 준비

 

1) Component Layout

그럼 이제 React 와 Spring Boot 를 이용하여 To-do List 를 만들어 보자.

  • Front-end : React JS
  • Back-end : Spring Boot + Java 를 이용한 Rest Api Server

 

개발에 앞서 사전 설계의 중요성을 잊지 않기 위해서 To-do List 의 Component Layout 을 그려보았다.

 

 

아래 그림은 최종적으로 만들게 될 To-do List 화면이다.

 

 

현재 React 와 Rest Api Server 를 공부 중인 아무것도 모르는 어린아이와 같은 실력이기 때문에 자세한 설명은 생략!

아래의 코드를 따라서 개발하게 되면 바로 위의 그림과 같은 To-do List 가 어느새 만들어져 있을 것이다!

 

2) 작업환경 설정

  • Node JS : v12.14.0
  • Yarn : 1.22.0
  • Spring Tool Suite : Back-end 개발에 이용
  • Visual Studio Code : Front-end 개발에 이용

 

3) create-react-app 설치

Create React App 은 React 를 배우기에 간편한 환경이며, 시작하기에 최고의 방법인 새로운 싱글 페이지 애플리케이션이다. 이러한 이유로 페이스북에서 만든 react 프로젝트 생성 도구인 create-react-app 을 사용!

 

4) 프로젝트 생성

 

 

5) 프로젝트 실행

 

 

Local 또는 On your Network 의 URL 을 Chrome 에서 실행하게 되면 아래와 같은 화면이 보이게 된다.

 

 

6) 프로젝트 구조

다음으로 Visual Studio Code 에서 프로젝트 구조를 확인해보도록 하자.

create-react-app 으로 만든 프로젝트의 구조는 기본적으로 node_modules, public, src 폴더로 이루어져 있다.

앞으로 개발은 src 폴더 하위의 App.js 파일과 src 폴더 내 하위 폴더를 생성하여 Component 별 js 파일을 생성하여 개발을 진행할 것이다.

 

 

7) src/index.js

App 의 엔트리 포인트 역할을 하는 파일로, 리액트 루트 컴포넌트를 DOM 에 마운팅하는 역할을 한다.

코드 하단에 보면 JSX 문법을 사용하는데 React Library 를 로딩하면 이를 해석할 수 있다. 루트 컴포넌트 App 을 DOM 에 마운트하기 위해서는 react-dom Library 의 render() 함수를 사용한다.

 

8) 프로젝트 초기화

src/App.js 파일의 코드를 수정하여 To-do List 를 만들기 위한 준비를 하도록 하자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
 
class App extends React.Component {
    render() {
        return (
            <div>
                To-do List 를 만들어보겠습니다!
            </div>
        );
    }
}
 
export default App;

 

위와 같이 src/App.js 코드를 수정하게 되면 아래와 같이 화면이 바뀌는 것을 볼 수 있다.

 

 


 

2. 컴포넌트 구성하기

 

다음으로 위에서 그림으로 표현한 To-do List 의 Component Layout 을 토대로 컴포넌트를 구성하도록 한다.

우선 App 컴포넌트의 하위 Component 들인 TodoListTemplate, Form, TodoItemList, TodoItem 컴포넌트를 만들기 위해 src 폴더 하위 경로에 components 폴더를 생성하도록 한다.

 

 

컴포넌트 별로 각자의 폴더를 구성해도 되지만 이는 개발자 본인의 스타일에 따라서 결정하면 될 것 같다. 물론 프로젝트의 구조가 크고 복잡하다면 당연히 컴포넌트 별로 각자의 폴더를 만드는게 개발에 있어서 더 유리하겠지만~!

components 폴더를 만들었다면 components 폴더 하위에 js 폴더와 css 폴더를 추가하도록 한다~!

이유는 컴포넌트를 작성할 js 파일과 컴포넌트들의 스타일을 담당할 css 파일을 나눠주기 위함이다.

 

 

1) TodoListTemplate 컴포넌트

components 폴더가 생성되었다면 우선 App 컴포넌트의 하위 컴포넌트 중 가장 큰 단위인 TodoListTemplate 컴포넌트를 구성해야 한다.

  • TodoListTemplate.js 생성
  • TodoListTemplate.css 생성

src/components/js 폴더 경로에 TodoListTemplate 컴포넌트를 생성 후 아래 코드를 작성한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import '../css/TodoListTemplate.css';
 
const TodoListTemplate = ({ form, children }) => {
    return (
        <main className="todo-list-template">
            <div className="todo-list-title">
                오늘 할 일
            </div>
            <section className="form-wrapper">
                {form}
            </section>
            <section className="todoItemList-wrapper">
                {children}
            </section>
        </main>
    );
};
 
export default TodoListTemplate;

 

이 컴포넌트는 함수형 컴포넌트이다.

파라미터로 받게 되는 것은 props 이며, 이를 비구조화 할당하여 원래 (props) => { ... } 를 해야하는 것을 

({form, todoItemList}) => { ... } 형태로 작성을 한 것이다. 이 컴포넌트는 두 가지의 props 를 받게 되는데 children 의 경우엔 나중에 우리가 이 컴포넌트를 사용하게 될 때 아래와 같은 방법으로 사용하게 된다.

 

 
 
<TodoListTemplate>오늘 할 일 템플릿입니다</TodoListTemplate>
 

 

TodoListTemplate 태그 사이의 내용이 TodoListTemplate 컴포넌트의 children props 로 전달되게 된다.

 

 

TodoListTemplate 컴포넌트를 작성했다면 src/components/css 폴더 경로에 TodoListTemplate.css 파일을 만들고 아래 코드를 작성한다.

 

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
.todo-list-template {
    background: white;
    width: 90%;
    box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); /* 그림자 */ 
    margin: 0 auto; /* 페이지 중앙 정렬 */
    margin-top: 4rem;
}
  
.todo-list-title {
    padding: 2rem;
    font-size: 2.5rem;
    text-align: center;
    font-weight: 100;
    background: #353b54e0;
    color: white;
}
  
.form-wrapper {
    padding: 1rem;
    /* border-bottom: 1px solid #22b8cf; */
    border-bottom: 1px solid #353b54e0;
}
  
.todoItemList-wrapper {
    min-height: 5rem;
}

 

TodoListTemplate.css 파일까지 작성을 완료하였다면 TodoListTemplate 컴포넌트를 App 컴포넌트에서 불러오기 위해서 App 컴포넌트를 다음과 같이 수정한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import TodoListTemplate from './components/js/TodoListTemplate';
 
class App extends React.Component {
    render() {
        return (
            <TodoListTemplate>
                오늘 할 일 템플릿입니다
            </TodoListTemplate>
        );
    }
}
 
export default App;

 

App 컴포넌트를 수정했다면 이제 화면을 확인해본다.

 

 

화면이 다음과 같이 변경되었음을 확인할 수 있으며, 위에 언급한 TodoListTemplate 컴포넌트의 children props 가 잘 전달되었는지 확인해보자.

 

 

App 컴포넌트 하위에 TodoListTemplate 컴포넌트가 존재하며, TodoListTemplate 컴포넌트의 props 인 children 으로 "오늘 할 일 템플릿입니다" 라는 값이 들어왔음을 확인할 수 있다.

다음과 같이 크롬 웹브라우저에서 REACT 디버깅을 하려면 아래의 사이트를 따라하면 된다!

크롬 웹브라우저에서 REACT 디버깅툴 설치하기

 

2) Form 컴포넌트

Form 컴포넌트는 Input 과 Button 이 있는 컴포넌트이다.

앞으로 리액트 컴포넌트를 구현할 때는 아래와 같은 흐름으로 개발하게 된다.

 

 

TodoListTemplate 컴포넌트의 하위 컴포넌트인 Form 컴포넌트를 구성한다.

  • TodoListTemplate.js 생성
  • TodoListTemplate.css 생성

src/components/js 폴더 경로에 Form 컴포넌트를 생성 후 아래 코드를 작성한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import '../css/Form.css';
 
const Form = ({ value, onChange, onCreate, onKeyPress }) => {
    return (
        <div className="form">
            <input
                value={value}
                placeholder="오늘 할 일을 입력하세요.."
                onChange={onChange}
                onKeyPress={onKeyPress} />
            <div className="create-button" onClick={onCreate}>
                추가
            </div>
        </div>
    );
};
 
export default Form;

 

Form 컴포넌트는 총 4가지의 props 를 받는다.

  • value : Input 내용
  • onChange : Input 내용이 변경될 때 실행되는 함수
  • onCreate : 버튼이 클릭될 때 실행되는 함수
  • onKeyPress : 인풋에서 키를 입력할 때 실행되는 함수로 추후에 Enter Key Event 로 onCreate 와 동일한 작업을 위한 함수

Form 컴포넌트를 작성했다면 src/components/css 폴더 경로에 Form.css 파일을 만들고 아래 코드를 작성한다.

 

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
.form {
    display: flex;
  }
  
.form input {
    flex: 1; /* 버튼을 뺀 빈 공간을 모두 채워줍니다 */
    font-size: 1.25rem;
    outline: none;
    border: none;
    border-bottom: 1px solid #353b54e0;
}
  
.create-button {
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
    padding-left: 1rem;
    padding-right: 1rem;
    margin-left: 1rem;
    background: #353b54e0;
    border-radius: 3px;
    color: white;
    font-weight: 600;
    cursor: pointer;
}
  
.create-button:hover {
    background: #353b54e0;
}

 

Form.css 파일까지 작성을 완료하였다면 Form 컴포넌트를 App 컴포넌트에서 불러오기 위해서 App 컴포넌트를 다음과 같이 수정한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import TodoListTemplate from './components/js/TodoListTemplate';
import Form from './components/js/Form';
 
class App extends React.Component {
    render() {
        return (
            <TodoListTemplate form={<Form />}>
                오늘 할 일 템플릿입니다
            </TodoListTemplate>
        );
    }
}
 
export default App;

 

App 컴포넌트를 수정했다면 이제 화면을 확인해본다.

 

 

화면이 다음과 같이 변경되었음을 확인할 수 있으며, TodoListTemplate 컴포넌트의 form props 가 잘 전달되었는지 확인해보자.

 

 

App 컴포넌트 하위에 TodoListTemplate 컴포넌트가 존재하며, TodoListTemplate 컴포넌트의 props 인 form 으로 Form 컴포넌트가 잘 들어왔음을 확인할 수 있다.

 

3) TodoItemList 컴포넌트

TodoItemList 컴포넌트는 TodoItem 컴포넌트 여러 개를 렌더링해주는 역할을 한다.

동적인 '리스트'를 렌더링을 하는 경우에는 함수형이 아닌 클래스형 컴포넌트로 작성하는 것이 컴포넌트 성능 최적화를 하기에 유리하다.

 

이제 TodoListTemplate 컴포넌트의 하위 컴포넌트인 TodoItemList 컴포넌트를 구성한다.

  • TodoItemList.js 생성
  • TodoItemList.css 생성

src/components/js 폴더 경로에 TodoItemList 컴포넌트를 생성 후 아래 코드를 작성한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
 
class TodoItemList extends React.Component {
    render() {
        const {todos, onToggle, onRemove} = this.props;
 
        return (
            <div>
                TodoItem 자리
            </div>
        );
    }
}
 
export default TodoItemList;

 

TodoItemList 컴포넌트는 3가지 props 를 받는다.

  • todos: todo 객체들이 들어있는 배열
  • onToggle : 체크박스를 on/off 하는 함수
  • onRemove : todo 객체를 삭제하는 함수

TodoItemList 컴포넌트를 작성했다면 TodoItemList 컴포넌트를 App 컴포넌트에서 불러오기 위해서 App 컴포넌트를 다음과 같이 수정한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import TodoListTemplate from './components/js/TodoListTemplate';
import Form from './components/js/Form';
import TodoItemList from './components/js/TodoItemList';
 
class App extends React.Component {
    render() {
        return (
            <TodoListTemplate form={<Form />}>
                <TodoItemList />
            </TodoListTemplate>
        );
    }
}
 
export default App;

 

App 컴포넌트를 수정했다면 이제 화면을 확인해본다.

 

 

화면이 다음과 같이 변경되었음을 확인할 수 있으며, TodoListTemplate 컴포넌트의 children props 가 잘 전달되었는지 확인해보자.

 

 

App 컴포넌트 하위에 TodoListTemplate 컴포넌트가 존재하며, TodoListTemplate 컴포넌트의 props 인 children 으로 TodoItemList 컴포넌트가 잘 들어왔음을 확인할 수 있다.

 

4) TodoItem 컴포넌트

TodoItem 컴포넌트는 체크 값이 활성화되어 있으면 우측에 체크마크( &#x2713;)를 보여주고 마우스가 위에 있을 때에는 좌측에 엑스마크(× &times;)를 보여준다.

이 컴포넌트의 영역이 클릭되면 체크박스가 활성화되며 중간줄이 그어지며 좌측의 엑스가 클릭되면 삭제된다.

** 모바일 환경에서는 TodoItem 을 선택하지 않은 이상 ..........

 

이제 TodoItemList 컴포넌트의 하위 컴포넌트인 TodoItem 컴포넌트를 구성한다.

  • TodoItem.js 생성
  • TodoItem.css 생성

src/components/js 폴더 경로에 TodoItem 컴포넌트를 생성 후 아래 코드를 작성한다.

 

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
import React from 'react';
import '../css/TodoItem.css';
 
class TodoItem extends React.Component {
    render() {
        const {content, isComplete, id, onToggle, onRemove} = this.props;
 
        return (
            <div className="todo-item" onClick={() => onToggle(id)}>
                <div className="todo-item-remove" onClick={(e) => {
                    e.stopPropagation();    // onToggle 이 실행되지 않도록 함
                    onRemove(id)}
                }>
                    &times;
                </div>
                <div className={`todo-item-content ${isComplete && 'isComplete'}`}>
                    <div>
                       {content}
                    </div>
                </div>
                {
                    isComplete && (<div className="isComplete-mark"></div>)
                }
            </div>
        )
    }
}
 
export default TodoItem;

 

TodoItem 컴포넌트는 5가지 props 를 받는다.

  • content: todo 내용
  • isComplete : 체크박스 on/off 상태를 의미하며, 오늘 할 일의 완료 유무를 판단
  • id : TodoItem 의 Key 값
  • onToggle : 체크박스를 on/off 시키는 함수
  • onRemove : TodoItem 을 삭제시키는 함수

 

위 코드의 Line 9 와 Line 10 을 보면, 해당 컴포넌트의 최상위 DOM 의 클릭 이벤트에는 onToggle 을 설정하고, x 가 있는 부분에는 onRemove 를 설정해주었다.

또한 onRemove 를 호출하는 곳에는 e.stopPropagation() 이라는 함수가 호출된 것을 확인할 수 있다.

만약에 이 함수를 호출하지 않으면 x 를 눌렀을 때 onRemove 만 실행되는 것이 아니라 해당 DOM 의 부모의 클릭 이벤트에 연결되어 있는 onToggle 도 실행되게 된다.

즉 onRemove → onToggle 순으로 함수가 호출되면서 코드가 의도치 않게 작동하여 삭제가 제대로 진행되지 않게 된다.

 

e.stopPropagation() 은 이벤트의 확산을 멈춰준다. 즉, 삭제 부분에 들어간 이벤트가 해당 부모의 이벤트까지 전달되지 않도록 해준다. 따라서 onToggle 은 실행되지 않고 onRemove 만 실행된다.

 

더보기

onToggle 과 onRemove 는 id 를 파라미터로 가지며, 해당 id 를 가진 데이터를 업데이트 한다. 파라미터를 넣어줘야 하기 때문에 이 과정에서 onClick={() => onToggle(id)} 와 같은 형식으로 작성을 하였다.

만약 onClick={onToggle{id}} 와 같은 형식으로 하고 싶다면 절대 안된다!! 리액트는 그렇다...

이렇게 하면 해당 함수가 렌더링 될 때 호출이 된다. 해당 함수가 호출되면 데이터가 변경 될 것이고 데이터가 변경되면 리렌더링이 일어나게 되고 또 이 함수가 호출되고.. 무한 반복이 일어나게 된다.

 

todo-item-content 를 보면 isComplete 값에 따라서 className 에 isComplete 라는 문자열을 추가하는 것을 확인할 수 있다. CSS 클래스를 유동적으로 설정하고 싶다면 이렇게 템플릿 리터럴 을 사용하면 된다.

 

 
 
`todo-item-content ${isComplete && 'isComplete'}`
 
"todo-item-content " + isComplete && 'isComplete'
 

 

위와 같이 하면 편리하긴 하지만 isComplete 값이 false 일 때는 todo-item-content false 와 같은 결과 값이 나타나게 된다. 큰 의미는 없지만 이 부분까지 고쳐준다면 아래와 같이 작성해야 한다.

 

 
 
`todo-item-content ${isComplete ? 'isComplete' : ' ' }`
 

 

TodoItem 컴포넌트를 작성했다면 src/components/css 폴더 경로에 TodoItem.css 파일을 만들고 아래 코드를 작성한다.

 

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
.todo-item {
    padding: 1rem;
    display: flex;
    align-items: center; /* 세로 가운데 정렬 */
    cursor: pointer;
    transition: all 0.15s;
    user-select: none;
    background-color: whitesmoke;
    margin: 0.3rem;
    border-radius: 5px 5px 5px 5px;
}
  
.todo-item:hover {
    background: #353b544d;
}
  
/* todo-item 에 마우스가 있을때만 .remove 보이기 */
.todo-item:hover .todo-item-remove {
    opacity: 1;
}
  
/* todo-item 사이에 윗 테두리 */
.todo-item + .todo-item {
    border-top: 1px solid #f1f3f5;
}
  
.todo-item-remove {
    margin-right: 1rem;
    color: #e64980;
    font-weight: 600;
    opacity: 0;
}
  
.todo-item-content {
    flex: 1; /* 체크, 엑스를 제외한 공간 다 채우기 */
    word-break: break-all;
}
  
.isComplete {
    text-decoration: line-through;
    color: #adb5bd;
}
  
.isComplete-mark {
    font-size: 1.5rem;
    line-height: 1rem;
    margin-left: 1rem;
    color: #353b54e0;;
    font-weight: 800;
}

 

TodoItem.css 파일까지 작성을 완료하였다면 TodoItem 컴포넌트를 TodoItemList 컴포넌트에서 불러오기 위해서 TodoItemList 컴포넌트를 다음과 같이 수정한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import TodoItem from './TodoItem';
 
class TodoItemList extends React.Component {
    render() {
        const {todos, onToggle, onRemove} = this.props;
 
        return (
            <div>
                <div>
                    <TodoItem content="TodoItem1" />
                    <TodoItem content="TodoItem2" />
                    <TodoItem content="TodoItem3" />
                </div>
            </div>
        );
    }
}
 
export default TodoItemList;

 

TodoItemList 컴포넌트를 수정했다면 이제 화면을 확인해본다.

 

 

화면이 다음과 같이 변경되었음을 확인할 수 있으며, TodoItemList 컴포넌트의 하위 컴포넌트로 TodoItem 컴포넌트가 들어왔고 TodoItem 컴포넌트의 content props 가 잘 전달되었는지 확인해보자.

 

 

TodoItemList 컴포넌트 하위에 TodoItem 컴포넌트가 존재하며, TodoItem 컴포넌트의 props 인 content 에 TodoItemList 에서 보낸 값이 잘 들어있음을 확인할 수 있다.

 


 

3. 컴포넌트 이벤트 설정

 

1) State 관리하기

리액트 애플리케이션 안에서 변경이 일어나는 데이터에 대해서는 "진리의 원천(source of truth)"을 하나만 두어야 한다. 보통의 경우, state 는 렌더링에 그 값을 필요로 하는 컴포넌트에 먼저 추가된다. 그러고 나서 다른 컴포넌트도 역시 그 값이 필요하게 되면 그 값을 그들의 가장 가까운 공통 조상으로 끌어올리면 된다. 다른 컴포넌트 간에 존재하는 state 를 동기화시키려고 노력하는 대신 하향식 데이터 흐름에 기대는 것을 추천한다.

 

이 프로젝트에서 state가 필요한 컴포넌트는 데이터를 입력 받는 Form 과 변경된 데이터를 출력할 TodoItemList 컴포넌트이다. 리액트에 익숙하지 않다면 리액트의 state 를 아래의 그림과 같이 각 컴포넌트에 넣어주려고 할 것이다.

 

 

위에 언급한 바와 같이 리액트에서는 이러한 구조는 피하는 것이 좋다.

 

대신에 위의 Component Layout 에서 볼 수 있듯이 Form 컴포넌트와 TodoItemList 컴포넌트의 부모 컴포넌트인 App 컴포넌트에 Form 컴포넌트의 input, TodoItemList 컴포넌트의 todos 에 대한 state 를 넣어주고 이를 각각 컴포넌트에 props 로 전달해주는 기능을 구현하도록 한다.

 

 

2) State 정의

App 컴포넌트를 수정하여 state 를 정의하도록 한다.

 

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
import React from 'react';
import TodoListTemplate from './components/js/TodoListTemplate';
import Form from './components/js/Form';
import TodoItemList from './components/js/TodoItemList';
 
class App extends React.Component {
    constructor(props) {
        super(props);
        const id = 2;
        this.state = {
            input: "",
            todos: [
                { id: 0, content: '리액트를 공부하자0', isComplete: false },
                { id: 1, content: '리액트를 공부하자1', isComplete: true }
            ]
        }
    }
 
    render() {
        return (
            <TodoListTemplate form={<Form />}>
                <TodoItemList />
            </TodoListTemplate>
        );
    }
}
 
export default App;

 

3) Form 컴포넌트 기능 구현

Form 컴포넌트에 필요한 기능은 아래와 같다.

  • input 값이 변경되면 state 업데이트
  • 버튼이 클릭되면 새로운 todo 생성 후 todos 업데이트
  • input 박스에서 Enter 키를 누르면 버튼 이벤트와 동일하게 새로운 todo 생성 후 todos 업데이트

위 기능을 위해서 App 컴포넌트에 handleChange, handleCreate, handlekeyPress Method 를 구현하고 이를 state 의 input 과 함께 Form 컴포넌트로 전달한다.

 

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
import React from 'react';
import TodoListTemplate from './components/js/TodoListTemplate';
import Form from './components/js/Form';
import TodoItemList from './components/js/TodoItemList';
 
class App extends React.Component {
    constructor(props) {
        super(props);
        this.id = 2;
        this.state = {
            input: "",
            todos: [
                { id: 0, content: '리액트를 공부하자0', isComplete: false },
                { id: 1, content: '리액트를 공부하자1', isComplete: true }
            ]
        }
        this.handleChange = this.handleChange.bind(this);
        this.handleCreate = this.handleCreate.bind(this);
        this.handleKeyPress = this.handleKeyPress.bind(this);
    }
 
    handleChange(event) {
        this.setState({
            input: event.target.value
        });
    }
 
    handleCreate() {
        const { input, todos } = this.state;
        if (input === "") {
            alert("오늘 할 일을 입력해주세요!");
            return;
        }
        this.setState({
            input: "",
            todos: todos.concat({
                id: this.id++,
                content: input,
                isComplete: false
            })
        });
    }
 
    handleKeyPress(event) {
        if (event.key === "Enter") {
            this.handleCreate();
        }
    }
 
    render() {
        return (
            <TodoListTemplate form={(
                <Form
                    value={this.state.input}
                    onChange={this.handleChange}
                    onCreate={this.handleCreate}
                    onKeyPress={this.handleKeyPress} />
            )}>
                <TodoItemList />
            </TodoListTemplate>
        );
    }
}
 
export default App;

 


 

Check Point !!!

 

handleCreate 메서드를 보면 concat 을 사용하여 배열안에 데이터를 추가하고 있다. 자바스크립트로 보통 배열안에 새 데이터를 집어넣을 경우에는 주로 push 를 사용하는 것과 차이점을 보인다.

 

const array = [ ];

array.push(1);

// [ 1 ]

 

리액트 state 에서 배열을 다룰 경우 절대로 push 함수를 사용하면 안된다.

그 이유는 다음과 같다.

 

let array1 = [ ];

let array2 = array1;

array1.push(1);

console.log(array1 === array2);

// true

 

push 를 통하여 데이터를 추가하면 배열에 값이 추가되긴 하지만 가르키고 있는 배열은 똑같기 때문에 비교를 할 수 없게 된다. 나중에 최적화 시 배열을 비교하여 리렌더링을 방지를 하게 되는데 push 를 사용한다면 최적화를 할 수 없게 되기 때문이다.

 

반면 concat 의 경우엔 새 배열을 만들기 때문에 배열을 비교함에 있어서 문제가 없다.

 

let array1 = [ ];

let array2 = array1.concat(1);

console.log(array1 === array2);

// false

 


 

4) TodoItemList 컴포넌트에서 배열을 TodoItem 컴포넌트 배열로 변환하기

todos 안에 있는 객체들을 화면에 보여주기 위해선 todos 배열을 컴포넌트 배열로 변환해줘야 한다. 배열을 변환할 때는 자바스크립트 배열의 내장함수 map 을 사용한다.

 

// 배열안의 원소를 모두 제곱하기

const numbers = [1, 3, 5, 7, 9];

const squared = numbers.map(number => number * number);

console.log(numbers);

// [1, 9, 25, 49, 81]

 

5) TodoItemList 컴포넌트에 todos 전달하기

App 컴포넌트의 render 함수를 수정한다.

 

render() {

    return (

        <TodoListTemplate form={(

            <Form

                value={this.state.input}

                onChange={this.handleChange}

                onCreate={this.handleCreate}

                onKeyPress={this.handleKeyPress} />

        )}>

            <TodoItemList todos={this.state.todos} />

        </TodoListTemplate>

    );

}

 

6) TodoItemList 컴포넌트의 객체배열을 컴포넌트 배열로 변환하기

 

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
import React from 'react';
import TodoItem from './TodoItem';
 
class TodoItemList extends React.Component {
    render() {
        const {todos, onToggle, onRemove} = this.props;
 
        const todoList = todos.map (
            ({id, content, isComplete}) => (
                <TodoItem
                    id={id}
                    content={content}
                    isComplete={isComplete}
                    onToggle={onToggle}
                    onRemove={onRemove}
                    key={id} />
            )
        );
 
        return (
            <div>
                {todoList}
            </div>
        );
    }
}
 
export default TodoItemList;

 

const todoList = todos.map(todo => ...) 의 형태로 작성할 수 있지만 함수의 파라미터 부분에서 비구조화 할당을 하여 객체 내부의 값들을 따로 레퍼런스를 만들어주었다.

 

배열을 렌더링할 때에는 key 값이 반드시 있어야한다. 없는 경우에 map 함수의 두 번째 파라미터인 index 를 사용하면된다. 하지만 index 를 key 로 사용하는 것은 정말 필요한 상황이 아니면 권장하지 않는다. key 값이 있어야만 컴포넌트가 리렌더링 될 때 더욱 효율적으로 작동할 수 있다.

 


Check Point !!!

 

다음과 같이 객체의 값을 {...todo} 와 같이 모두 props 로 전달할 수 있으며, 내부의 값들이 자동으로 props 로 설정이 된다.

 

const todoList = todos.map (

    (todo) => (

        <TodoItem

            {...todo}

            onToggle={onToggle}

            onRemove={onRemove}

            key={todo.id} />

    )

);

 


 

TodoItemList 컴포넌트를 수정했다면 이제 화면을 확인해본다.

 

 

input 에 새로운 값을 입력 후 추가할 경우 TodoItemList 컴포넌트의 하위에 TodoItem 컴포넌트가 추가 생성되었음을 확인할 수 있다.

 

 

7) TodoItem 컴포넌트 이벤트 추가

App 컴포넌트에 handleToggle Method 를 추가하여 '오늘 할 일' 을 완료했을 경우에 대한 이벤트를 설정한다.

 

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
import React from 'react';
import TodoListTemplate from './components/js/TodoListTemplate';
import Form from './components/js/Form';
import TodoItemList from './components/js/TodoItemList';
 
class App extends React.Component {
    constructor(props) {
        super(props);
        this.id = 2;
        this.state = {
            input: "",
            todos: [
                { id: 0, content: '리액트를 공부하자0', isComplete: false },
                { id: 1, content: '리액트를 공부하자1', isComplete: true }
            ]
        }
        this.handleChange = this.handleChange.bind(this);
        this.handleCreate = this.handleCreate.bind(this);
        this.handleKeyPress = this.handleKeyPress.bind(this);
        this.handleToggle = this.handleToggle.bind(this);
    }
 
    handleChange(event) {
        this.setState({
            input: event.target.value
        });
    }
 
    handleCreate() {
        const { input, todos } = this.state;
        if (input === "") {
            alert("오늘 할 일을 입력해주세요!");
            return;
        }
        this.setState({
            input: "",
            todos: todos.concat({
                id: this.id++,
                content: input,
                isComplete: false
            })
        });
    }
 
    handleKeyPress(event) {
        if (event.key === "Enter") {
            this.handleCreate();
        }
    }
 
    handleToggle(id) {
        const todos = this.state.todos;
 
        const isComplete = todos.find(todo => todo.id === id).isComplete;
        if(!window.confirm(isComplete ? "미완료 처리 하시겠습니까?" : "완료 처리 하시겠습니까?")) {
            return;
        }
 
        // 파라미터로 받은 id 를 가지고 몇 번째 아이템인지 찾는다.
        const index = todos.findIndex(todo => todo.id === id);
 
        // 선택한 객체를 저장한다.
        const selected = todos[index];
 
        // 배열을 복사한다.
        const nextTodos = [...todos];
 
        // 기존의 값을 복사하고 isComplete 값을 덮어쓴다.
        nextTodos[index] = {
            ...selected,
            isComplete : !selected.isComplete
        };
 
        this.setState({
            todos : nextTodos
        });
    }
 
    render() {
        return (
            <TodoListTemplate form={(
                <Form
                    value={this.state.input}
                    onChange={this.handleChange}
                    onCreate={this.handleCreate}
                    onKeyPress={this.handleKeyPress} />
            )}>
                <TodoItemList
                    todos={this.state.todos}
                    onToggle={this.handleToggle} />
            </TodoListTemplate>
        );
    }
}
 
export default App;

 


 

Check Point !!!

 

배열을 업데이트 할 때에도 마찬가지로 배열의 값을 직접 수정하면 절대 안된다. push 를 사용하면 안되는 이유와 동일하다.

 

let array = [{value : 1}, {value : 2}];

let nextArray = array;

nextArray[0].value = 10;

console.log(array === nextArray)

// true

 


 

App 컴포넌트에 handleRemove Method 를 추가하여 '오늘 할 일' 을 제거하는 이벤트를 추가한다.

이때 자바스키립트 배열의 내장함수인 filter 를 사용하여 파라미터로 받은 id 를 제외한 새로운 배열을 생성한다.

 

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
101
102
103
104
105
106
107
108
109
110
111
import React from 'react';
import TodoListTemplate from './components/js/TodoListTemplate';
import Form from './components/js/Form';
import TodoItemList from './components/js/TodoItemList';
 
class App extends React.Component {
    constructor(props) {
        super(props);
        this.id = 2;
        this.state = {
            input: "",
            todos: [
                { id: 0, content: '리액트를 공부하자0', isComplete: false },
                { id: 1, content: '리액트를 공부하자1', isComplete: true }
            ]
        }
        this.handleChange = this.handleChange.bind(this);
        this.handleCreate = this.handleCreate.bind(this);
        this.handleKeyPress = this.handleKeyPress.bind(this);
        this.handleToggle = this.handleToggle.bind(this);
        this.handleRemove = this.handleRemove.bind(this);
    }
 
    handleChange(event) {
        this.setState({
            input: event.target.value
        });
    }
 
    handleCreate() {
        const { input, todos } = this.state;
        if (input === "") {
            alert("오늘 할 일을 입력해주세요!");
            return;
        }
        this.setState({
            input: "",
            todos: todos.concat({
                id: this.id++,
                content: input,
                isComplete: false
            })
        });
    }
 
    handleKeyPress(event) {
        if (event.key === "Enter") {
            this.handleCreate();
        }
    }
 
    handleToggle(id) {
        const todos = this.state.todos;
 
        const isComplete = todos.find(todo => todo.id === id).isComplete;
        if(!window.confirm(isComplete ? "미완료 처리 하시겠습니까?" : "완료 처리 하시겠습니까?")) {
            return;
        }
 
        // 파라미터로 받은 id 를 가지고 몇 번째 아이템인지 찾는다.
        const index = todos.findIndex(todo => todo.id === id);
 
        // 선택한 객체를 저장한다.
        const selected = todos[index];
 
        // 배열을 복사한다.
        const nextTodos = [...todos];
 
        // 기존의 값을 복사하고 isComplete 값을 덮어쓴다.
        nextTodos[index] = {
            ...selected,
            isComplete : !selected.isComplete
        };
 
        this.setState({
            todos : nextTodos
        });
    }
 
    handleRemove(id) {
        const todos = this.state.todos;
 
        const removeContent = todos.find(todo => todo.id === id).content;
        if(!window.confirm("'" + removeContent + "' 을 삭제하시겠습니까?")) {
            return;
        }
 
        this.setState({
            todos : todos.filter(todo => todo.id !== id)
        });
    }
 
    render() {
        return (
            <TodoListTemplate form={(
                <Form
                    value={this.state.input}
                    onChange={this.handleChange}
                    onCreate={this.handleCreate}
                    onKeyPress={this.handleKeyPress} />
            )}>
                <TodoItemList
                    todos={this.state.todos}
                    onToggle={this.handleToggle}
                    onRemove={this.handleRemove} />
            </TodoListTemplate>
        );
    }
}
 
export default App;

 

App 컴포넌트를 수정했다면 이제 화면을 확인해본다.

 

handleToggle 이벤트 호출 전

 

handleToggle 이벤트 호출 후

 

handleRemove 이벤트 호출 후

 


 

4. 컴포넌트 최적화

 

1) TodoItemList 컴포넌트 최적화

현재 리액트 컴포넌트들이 빠르게 렌더링이 되고 있지만 자원이 낭비되고 있는 부분이 존재한다.

TodoItem 컴포넌트의 render 함수를 다음과 같이 수정해보자.

 

class TodoItem extends React.Component {

    render() {

        const { content, isComplete, id, onToggle, onRemove } = this.props;

        console.log(id);

    }

}

 

TodoItem 컴포넌트의 render 함수를 수정하였다면, Chrome 의 개발자 도구의 콘솔을 열고 input 값을 수정해보자.

 

 

값을 입력할 때마다 render 함수가 실행되고 있음을 알 수 있다.

우선 render 함수가 실행된다고 해서 DOM 에 변화가 일어나는 것은 아니며, 리액트는 가상 DOM 을 사용하기 때문에 변화가 없는 곳은 그대로 두게된다. 다만 가상 DOM 에 렌더링하는 자원이 낭비가 되고 있다고 볼 수 있다.

업데이트가 불필요하다면 render 를 아예 실행하지 않게 하는 것이 프로젝트 성능 최적화에 도움이 될 것이다.

 

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
import React from 'react';
import TodoItem from './TodoItem';
 
class TodoItemList extends React.Component {
 
    shouldComponentUpdate(nextProps, nextState) {
        return this.props.todos !== nextProps.todos;
    }
 
    render() {
        const { todos, onToggle, onRemove } = this.props;
 
        const todoList = todos.map(
            ({ id, content, isComplete }) => (
                <TodoItem
                    id={id}
                    content={content}
                    isComplete={isComplete}
                    onToggle={onToggle}
                    onRemove={onRemove}
                    key={id} />
            )
        );
 
        return (
            <div>
                {todoList}
            </div>
        );
    }
}
 
export default TodoItemList;

 

컴포넌트 라이프 사이클 메서드 중 shouldComponentUpdate 메서드는 컴포넌트가 리렌더링을 할지 말지를 결정해준다. 이 메서드를 따로 구현하지 않으면 언제나 true 를 반환하기 때문에 이를 구현하는 경우에는 업데이트에 영향을 끼치는 조건을 return 해주면 된다.

즉 todos 값이 변경될 때 리렌더링이 일어나도록 this.props.todos 와 nextProps.todos 를 비교하여 이 값이 다를 경우에만 리렌더링을 하도록 설정하면 된다.

 

input 에 값을 입력하여도 크롬의 콘솔창에 변화가 없음을 알 수 있다.
추가 버튼을 클릭한 경우에 크롬의 콘솔창에 2라는 메시지가 출력된다.

 

2) TodoItem 컴포넌트 최적화

TodoItemList 컴포넌트와 마찬가지로 TodoItem 컴포넌트도 최적화가 필요하다.

TodoItem 컴포넌트 isComplete 값의 변경 및 TodoItem 컴포넌트 추가 및 삭제의 경우에도 모든 컴포넌트가 렌더링되고 있기 때문이다.

 

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
import React from 'react';
import '../css/TodoItem.css';
 
class TodoItem extends React.Component {
 
    shouldComponentUpdate(nextProps, nextState) {
        return this.props.isComplete !== nextProps.isComplete;
    }
 
    render() {
        const { content, isComplete, id, onToggle, onRemove } = this.props;
 
        return (
            <div className="todo-item" onClick={() => onToggle(id)}>
                <div className="todo-item-remove" onClick={(e) => {
                    e.stopPropagation();    // onToggle 이 실행되지 않도록 함
                    onRemove(id)
                }
                }>
                    &times;
                </div>
                <div className={`todo-item-text ${isComplete && 'isComplete'}`}>
                    <div>
                        {content}
                    </div>
                </div>
                {
                    isComplete && (<div className="isComplete-mark"></div>)
                }
            </div>
        )
    }
}
 
export default TodoItem;

 

TodoItemList 컴포넌트와 마찬가지로 TodoItem 컴포넌트도 shouldComponentUpdate 메서드를 사용하여 컴포넌트가 리렌더링을 할지 말지를 결정해주면 된다.

확인은 크롬브라우저의 개발자 모드를 활성화 후 직접 확인해보시길!

 

 

 

지금까지 리액트를 이용하여 Todo-List 프로젝트의 Front-end 개발을 진행하였다.

그냥 개발만 하면 어떻게든 하겠지만... 블로그를 작성하려고 하니 어떻게 글을 작성해야하고 진행 순서를 잡기가 너무 어려웠다. 어쩔 수 없이... 모방은 창조의 어머니라고 하니깐... 아래의 블로그를 거의 배끼는 수준으로... 참조하였다... 

죄송합니다. 그리고 감사합니다.

React 기초 입문 프로젝트 - 흔하디 흔한 할 일 목록 만들기

 

 

다음 포스팅은 Spring Boot 를 이용한 Back-end 개발을 진행할 것이다.

state가 변경되거나 부모 컴포넌트로부터 새로운 props를 전달받을 때 실행됩니다. React는 이 메소드(shouldComponentUpdate)의 반환 값에 따라서 re-render를 할지에 대한 여부를 결정

 

 


 

[ 수정사항 ]

1) TodoItemList 컴포넌트 최적화 를 위해서 컴포넌트 라이프 사이클 메서드 중 shouldComponentUpdate 메서드를 사용하였다.

이는 state 가 변경되거나 부모 컴포넌트로부터 새로운 props 를 전달받을 때 실행되게 되는데 React 는 이 메서드의 반환 값에 따라거 re-render 를 할지에 대한 여부를 결정하게 된다.

 

여기서 중요한 점은 Form 컴포넌트의 input 값이 입력될 때마다 render 함수가 실행되어 자원이 낭비되는 것을 막기위해 사용한 TodoItem 컴포넌트와 TodoItemList 컴포넌트의 shouldComponentUpdate() 조건이 todos 에서 isComplete 나 다른 조건으로 계속 변경된다면 관리하기가 어렵다는 것이다.

 

shouldComponentUpdate(nextProps, nextState) {

    return this.props.todos !== nextProps.todos;

}

 

이러한 문제점을 해결하기 위한 방법으로 Form 컴포넌트에 React Hook 을 적용하면 된다.

 

 

1) 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
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import React from 'react';
import TodoListTemplate from './components/js/TodoListTemplate';
import Form from './components/js/Form';
import TodoItemList from './components/js/TodoItemList';
 
class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            // *** Form.js 에서 Hook(useState) 사용으로 인해 제거
            // input : "",
            todos : [
 
            ]
        }
        // this.handleChange = this.handleChange.bind(this);
        this.handleCreate = this.handleCreate.bind(this);
        // this.handleKeyPress = this.handleKeyPress.bind(this);
        this.handleToggle = this.handleToggle.bind(this);
        this.handleRemove = this.handleRemove.bind(this);
        this.handleInitInfo = this.handleInitInfo.bind(this);
    }
 
 
    componentDidMount() {
        this.handleInitInfo()
    }
 
 
    handleInitInfo() {
        fetch("/api/todos")
            .then(res => res.json())
            .then(todos => this.setState({todos : todos}))
            .catch(err => console.log(err))
    }
 
 
    // *** Form.js 에서 Hook(useState) 사용으로 인해 제거
    // input 값 변경
    // handleChange(event) {
    //     this.setState({
    //         input: event.target.value
    //     });
    // }
 
 
    // *** Form.js 에서 Hook(useState) 사용으로 인해 state 에서 input 을 제외하고
    // parameter 로 받는다.
    // 등록
    handleCreate(inputValue) {
        const { todos } = this.state;
        if (inputValue === "") {
            alert("오늘 할 일을 입력해주세요!");
            return;
        }
 
        // 화면에서 먼저 변경사항을 보여주는 방법으로 이용
        this.setState({
            // input: "",
            // concat 을 사용하여 배열에 추가
            todos: todos.concat({
                id: 0,    // 임의의 id를 부여하여 key error 를 방지
                content: inputValue,
                isComplete: false
            })
        });
 
        // 처리
        const data = {
            body: JSON.stringify({"content" : inputValue}),
            headers: {'Content-Type''application/json'},
            method: 'post'
        }
        fetch("/api/todos", data)
            .then(res => {
                if(!res.ok) {
                    throw new Error(res.status);
                } else {
                    return this.handleInitInfo();
                }
            })
            .catch(err => console.log(err));
    }
 
 
    // *** Form.js 에서 Hook(useState) 사용으로 인해 제거
    // Enter Key 이벤트
    // handleKeyPress(event) {
    //     if (event.key === "Enter") {
    //         this.handleCreate();
    //     }
    // }
 
 
    // 수정
    handleToggle(id) {
        const { todos } = this.state;
 
        const isComplete = todos.find(todo => todo.id === id).isComplete;
        if(!window.confirm(isComplete ? "미완료 처리 하시겠습니까?" : "완료 처리 하시겠습니까?")) {
            return;
        }
 
        // 파라미터로 받은 id 를 가지고 몇 번째 아이템인지 찾는다.
        const index = todos.findIndex(todo => todo.id === id);
 
        // 선택한 객체를 저장한다.
        const selected = todos[index];
 
        // 배열을 복사한다.
        const nextTodos = [...todos];
 
        // 기존의 값을 복사하고 isComplete 값을 덮어쓴다.
        nextTodos[index] = {
            ...selected,
            isComplete : !selected.isComplete
        };
 
        this.setState({
            todos : nextTodos
        });
 
        const data = {
            headers: {'Content-Type':'application/json'},
            method: 'put'
        }
        fetch("/api/todos/" + id, data)
        .then(res => {
            if(!res.ok) {
                throw new Error(res.status);
            } else {
                return this.handleInitInfo();
            }
        })
        .catch(err => console.log(err));
    }
 
 
    // 삭제
    handleRemove(id) {
        const { todos } = this.state;
 
        const removeContent = todos.find(todo => todo.id === id).content;
        if(!window.confirm("'" + removeContent + "' 을 삭제하시겠습니까?")) {
            return;
        }
 
        this.setState({
            todos : todos.filter(todo => todo.id !== id)
        });
 
        const data = {
            headers: {'Content-Type':'application/json'},
            method: 'delete'
        }
        fetch("/api/todos/" + id, data)
        .then(res => {
            if(!res.ok) {
                throw new Error(res.status);
            } else {
                return this.handleInitInfo();
            }
        })
        .catch(err => console.log(err));
    }
 
 
    render() {
        return (
            <TodoListTemplate form={(
                <Form
                    // *** Form.js 에서 Hook(useState) 사용으로 인해 제거
                    // value={this.state.input}
                    // onChange={this.handleChange}
                    // onCreate={this.handleCreate}
                    // onKeyPress={this.handleKeyPress} 
                    onCreate={this.handleCreate}
                />
            )}>
                <TodoItemList
                    todos={this.state.todos}
                    onToggle={this.handleToggle}
                    onRemove={this.handleRemove} />
            </TodoListTemplate>
        );
    }
}
 
export default App;

 

 

2) Form 컴포넌트 수정

 

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
import React, { useState } from 'react';
import '../css/Form.css';
 
 
// *** Form.js 에서 Hook(useState) 사용으로 인해 수정
// const Form = ({ value, onChange, onCreate, onKeyPress }) => {
const Form = ({ onCreate }) => {
 
    // React Hook > 클래스 타입에서는 사용 X
    const [ input, setInput ] = useState('');
 
    // input 값 변경
    const handleChange = (event) => {
        setInput(event.target.value);
    }
 
    // Enter key event
    const handleKeyPress = (event) => {
        // 눌려진 키가 Enter key 인 경우 handleCreate 호출
        if(event.key === 'Enter') {
            onCreate(input);
            setInput('');
        }
    }
 
 
    return (
        <div className="form">
            <input
                value={input}
                placeholder="오늘 할 일을 입력하세요.."
                onChange={handleChange}
                onKeyPress={handleKeyPress} />
            <div className="create-button" onClick={() => {
                    onCreate(input);
                    setInput('');
                }
            }>
                추가
            </div>
        </div>
    );
};
 
export default Form;

 

3) TodoItemList 컴포넌트

 

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
import React from 'react';
import TodoItem from './TodoItem';
 
class TodoItemList extends React.Component {
 
    // *** Form.js 에서 Hook(useState) 사용으로 인해 제거
    // shouldComponentUpdate(nextProps, nextState) {
    //     return this.props.todos !== nextProps.todos;
    // }
 
    render() {
        const { todos, onToggle, onRemove } = this.props;
        console.log(todos);
 
        const todoList = todos.map(
            ({ id, content, isComplete }) => (
                <TodoItem
                    id={id}
                    content={content}
                    isComplete={isComplete}
                    onToggle={onToggle}
                    onRemove={onRemove}
                    key={id} />
            )
        );
 
        return (
            <div>
                {todoList}
            </div>
        );
    }
}
 
export default TodoItemList;

 

4) TodoItem 컴포넌트

 

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
import React from 'react';
import '../css/TodoItem.css';
 
class TodoItem extends React.Component {
 
    // *** Form.js 에서 Hook(useState) 사용으로 인해 제거
    // shouldComponentUpdate(nextProps, nextState) {
    //     return this.props.isComplete !== nextProps.isComplete;
    // }
 
    render() {
        const { content, isComplete, id, onToggle, onRemove } = this.props;
        console.log(id);
 
        return (
            <div className="todo-item" onClick={() => onToggle(id)}>
                <div className="todo-item-remove" onClick={(e) => {
                    e.stopPropagation();    // onToggle 이 실행되지 않도록 함
                    onRemove(id)
                }
                }>
                    &times;
                </div>
                <div className={`todo-item-text ${isComplete && 'isComplete'}`}>
                    <div>
                        {content}
                    </div>
                </div>
                {
                    isComplete && (<div className="isComplete-mark"></div>)
                }
            </div>
        )
    }
}
 
export default TodoItem;

 

위와 같이 수정 후 실행해보면 된다.

 

 

[ GitHub 주소 ]

https://github.com/JinhoHan/todolist_client

 

 

 

# 리액트를 공부합시다.