본문 바로가기

Programing/React

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

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

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

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

 

 

 

리액트로 만든 Front-end 와 Spring Boot 로 만든 Rest Api Server 를 연결해 보는 작업을 시작하겠다.

 


 

1. 화면 파일 수정

 

1) package.json 파일 수정

 

클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있도록 프록시 서버(Proxy Server) 를 설정해준다.

 

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
{
    "name""todolist",
    "version""0.1.0",
    "private"true,
    "dependencies": {
        "@testing-library/jest-dom""^4.2.4",
        "@testing-library/react""^9.3.2",
        "@testing-library/user-event""^7.1.2",
        "react""^16.13.0",
        "react-dom""^16.13.0",
        "react-scripts""3.4.0"
    },
    "scripts": {
        "start""react-scripts start",
        "build""react-scripts build",
        "test""react-scripts test",
        "eject""react-scripts eject"
    },
    "eslintConfig": {
        "extends""react-app"
    },
    "browserslist": {
        "production": [
            ">0.2%",
            "not dead",
            "not op_mini all"
        ],
        "development": [
            "last 1 chrome version",
            "last 1 firefox version",
            "last 1 safari version"
        ]
    },
    "proxy""http://localhost:8081"
}

 

2) 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
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.state = {
           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() {
        fetch("/api/todos")
            .then(res => res.json())
            .then(todos => this.setState({todos : todos}))
            .catch(err => console.log(err))
    }
 
    handleInitInfo() {
        fetch("/api/todos")
            .then(res => res.json())
            .then(todos => this.setState({todos : todos}))
            .catch(err => console.log(err))
    }
 
    // input 값 변경
    handleChange(event) {
        this.setState({
            input: event.target.value
        });
    }
 
    // 등록
    handleCreate() {
        const { input, todos } = this.state;
        if (input === "") {
            alert("오늘 할 일을 입력해주세요!");
            return;
        }
 
        // 화면에서 먼저 변경사항을 보여주는 방법으로 이용
        this.setState({
            input: "",
            // concat 을 사용하여 배열에 추가
            todos: todos.concat({
                id: 0,    // 임의의 id를 부여하여 key error 를 방지
                content: input,
                isComplete: false
            })
        });
 
        // 처리
        const data = {
            body: JSON.stringify({"content" : input}),
            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));
    }
 
    // Enter Key 이벤트
    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
        });
 
        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.todos;
 
        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
                    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;

 

  • Line 9 ~ 16 : 기존의 코드를 주석 처리한다. 서버를 연결하기 전에 화면 단에서 임의의 데이터를 미리 설정하기 위한 코드로 서버를 연결 후에는 필요 없는 데이터이다.
  • Line 31 : componentDidMount() 는 컴포넌트가 마운트 된 직후, 즉 트리에 삽입된 직후에 호출된다. DOM 노드가 있어야 하는 초기화 작업은 이 메스드에서 이루어진다. 외부에서 데이터를 불러와야 한다면, 네트워크 요청을 보내는 용도로 사용하면 된다.
  • Line 38 : handleInitInfo() 메서드 추가. 이 메서드는 등록, 수정, 삭제 후 목록을 다시 출력하기 위한 메서드이다.
  • Line 60 ~ 68 : '오늘 할 일' 을 추가로 등록할 경우 서버와의 작업이 완료될 때까지 기다리지 않고 화면에서 미리 등록 처리를 보여주기 위한 코드로 사용한다.
  • Line 71 ~ 76, 123 ~ 126, 151 ~ 154 : fetch 옵션을 설정한다.
  • Line 77, 127, 155 : fetch 함수를 이용하여 ajax 를 호출한다. ≪ 참조 ≫ 리액트 Fetch Document

 

 

3) App 컴포넌트 React Hook 적용으로 인한 수정.

 

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. 서버에서 테스트 데이터 생성 코드 삭제

 

서버 테스트 데이터 생성을 위해 사용한 CommandLineRunner 인터페이스를 삭제한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.bedmil.todolist;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class TodolistApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(TodolistApplication.class, args);
    }
 
}

 


 

3. 서버 실행

 

Front-end 와 Back-end 를 구동 후 화면에 접속하여 테스트를 진행한다.

 

리액트 Front-end 서버 실행은 아래와 같이 진행하면 된다.

 

C:\Users\bedmil\todolist> yarn start

 

아래와 같이 화면이 출력됨을 확인할 수 있다.

 

 


 

4. 테스트 진행

 

등록, 수정, 삭제 및 목록 출력에 이상이 없는지 테스트를 진행한다.

 

1) 등록

 

 

서버에서 자동 생성되는 ID(key) 값이 잘 생성되었고 데이터가 데이터가 잘 바인딩 되었는지 확인해본다.

 

 

2) 수정

 

오늘 할 일의 완료 여부를 수정한다.

 

 

TodoItem 컴포넌트를 확인하여 데이터의 값이 잘 변경되었는지 확인한다.

 

isComplete 값이 false 에서 true 로 변경되었다.

 

3) 삭제

 

데이터 삭제를 진행한다.

 

 

TodoItem 컴포넌트를 확인하여 데이터가 잘 삭제되었는지 확인한다.

 

key 값이 '3' 인 데이터가 삭제되었다.

 

4) 서버를 재구동 후 데이터가 H2 Database 에 잘 저장되어 있는지 확인한다.

 

인메모리가 아닌 file 로 관리하고 있으며 데이터가 손실되지 않았음을 확인

 


 

지금까지 리액트와 스프링부트를 이용한 '오늘 할 일(Todo-List)' 개발을 진행해보았다.

리액트를 공부하고 처음으로 만들어 본 프로젝트이기 때문에 다른 분의 포스팅을 참조하여 진행한 부분이 아쉽기는 하지만 리액트에 대한 이해를 할 수 있었던 프로젝트 였다.

추후에 기회가 된다면 이번에 진행한 Todo-List 의 Code Complete 와 기능 추가 구현을 해보도록 하겠다.

 

 

# 하면 된다.