React에서 데이터를 요청하면 요청한 데이터를 반환해주는 서버(Server) 개발환경을 구축해보자.
npm install express --save
Express 설치에 대한 자세한 내용은 다음 공식문서를 참고한다.
- 설치한 Node 모듈은 package.json파일 안에 있는 dependencies 부분에 추가된다. 이후 app 디렉토리에서 npm install을 실행하면 종속 항목 목록 내의 모듈이 자동으로 설치된다.
npm install cors
cors 란 Cross Origin Resource Sharing의 약자로, 현재 도메인과 다른 도메인으로 리소스가 요청될 경우를 말한다.
예를 들면, 도메인 http://AAA.com에서 읽어온 HTML페이지에서 다른 도메인 http://B.com/image.jpg를 요청하는 경우를 말한다. SPA(Single Page Application)상에서 해당하는 방법으로 데이터를 전달하기 위해 CORS를 사용하는 것이다.
npm install body-parser
body-parser
Express 문서에 따르면, 미들웨어 없이req.body
에 접근하는 경우에는 기본으로 undefined가 기본으로 설정되어 있으므로 bodyParser, multer와 같은 미들웨어를 사용하여 요청 데이터 값에 접근해야 한다. 때문에 body-parser로 요청받은 body값을 파싱하는 것이다.
// server.js
// Express / port / cors & body-parser --------------------------------- //
/* (1) Express 불러오기 */
const express = require('express');
const app = express(); // app 객체
/* (2) port번호 지정*/
const port = 4000;
/* (3) cors와 body-parser 불러오기*/
const cors = require('cors');
const bodyParser = require('body-parser');
// InitialTodoData (임시로 작성한 데이터, DB연동 예정) --------------------- //
const initialTodoData = require('../src/InitialTodoData.js');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors());
app.use(bodyParser.json());
// API ------------------------------------------------------------------ //
// GET
app.get('/initialtodos', (req, res) => {
// Response
res.send(initialTodoData);
})
// 외부로부터 요청을 받을 수 있도록 listen() -------------------------------- //
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
Server로부터 TodoList의 데이터를 받아오기 위해, useEffect 안에 fetching Data 로직을 작성해보자.
앞서 살펴본 useEffect는 여러 번 사용할 수 있다는 장점이 있다.
useEffect의 첫 번째 인자로는 todolist 데이터를 가져오는 내용을 작성해야 한다.
💡 여기서 주의할 점!
비동기 작업을 fetching 할 때에는 로직을 직접 넣는 것이 아니라, 그것을 처리하는 함수를 호출하는 것이 바람직하다. 버그나 UI 불일치 등을 야기할 수 있기 때문이다. [리액트 공식 가이드 참고]
다시 말해, useEffect의 콜백함수 안에 비동기 함수 로직을 직접적으로 작성하지 않아야 한다는 의미이다.
App.js
// App.js
import React, { useEffect, useState } from 'react';
import './App.css';
// components
import List from './components/List.jsx';
function App() {
// ---- useState ---- //
// 등록한 todo들을 담은 배열
const [todos, setTodos] = useState([]);
// 새로운 todo
const [newTodo, setNewTodo] = useState();
// changeInputData : newTodo에 input에 입력한 내용을 저장하는 함수
const changeInputData = (e) => {
setNewTodo(e.target.value);
}
// addTodo
const addTodo = (e) => {
e.preventDefault(); // 기본값 form 전송방지
setTodos([...todos, newTodo]);
}
// ---- useEffect ---- //
useEffect(() => {
console.log('새롭게 렌더링 되었습니다.');
// });
}, [todos]); // todos에 변경사항이 생기면 콜백함수 실행
// ---- fetching Data X useEffect ---- //
// initialtodos 데이터를 가져오는 함수
const fetchInitialData = async () => {
const response = await fetch('http://localhost:4000/initialtodos');
const initialData = await response.json();
setTodos(initialData);
}
// useEffect의 콜백함수에는 비동기 처리 로직을 직접 작성하면 X
// 빈 deps 배열 []은 useEffect가 componentDidMount()처럼 한 번만 실행되는 것을 의미
useEffect(()=>{
fetchInitialData();
}, []);
return(
<>
<h1>Todo Application</h1>
<form action="">
<input type="text" name="" onChange={changeInputData}/>
<button onClick={addTodo}>ADD</button>
</form>
<List todos={todos} />
</>
);
}
export default App;
List.jsx
Todo 데이터의 형식에 맞춰 List.js 파일도 수정해준다.
import React from 'react';
function List({todos}) {
const todoList = todos.map(todo => <li key={todo.id}>{todo.title}</li>);
return(
<ul>
{todoList}
</ul>
);
}
export default List;
초기 데이터가 렌더링되기까지의 1~2초 동안 로딩데이터를 보여주도록 하자.
이 loading의 상태를 List 컴포넌트의 props로 넘겨주고, 남은 작업들은 List 컴포넌트에서 처리하자.
App.js
// App.js
import React, { useEffect, useState } from 'react';
import './App.css';
// components
import List from './components/List.jsx';
function App() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState();
// loading 데이터
const [loading, setLoading] = useState(false);
const changeInputData = (e) => {
setNewTodo(e.target.value);
}
const addTodo = (e) => {
e.preventDefault(); // 기본값 form 전송방지
setTodos([...todos, newTodo]);
}
// ---- fetching Data X useEffect ---- //
// todolist 초기 데이터를 가져오는 함수
const fetchInitialData = async () => {
setLoading(true); // loading은 true
const response = await fetch('http://localhost:4000/initialtodos');
const initialData = await response.json();
setTodos(initialData);
setLoading(false); // loading은 false
}
useEffect(()=>{
fetchInitialData();
}, []);
return(
<>
<h1>Todo Application</h1>
<form action="">
<input type="text" name="" onChange={changeInputData}/>
<button onClick={addTodo}>ADD</button>
</form>
<List todos={todos} loading={loading}/>
</>
);
}
export default App;
List.jsx
List.jsx는 전달 받은 loading의 값이 true이면 "Loading..."을 출력하고,
전달 받은 loading의 값이 false이면 TodoList 항목을 브라우저에 렌더링하도록 한다.
// List.jsx
import React from 'react';
function List({todos, loading}) {
let todoList = <div>Loading...</div>;
if(!loading) todoList = todos.map(todo => <li key={todo.todoCode}>{todo.title}</li>);
return(
<ul>
{todoList}
</ul>
);
}
export default List;
결과를 확인해 보면, 새로 고침하였을 때 TodoList 목록이 나오기 전 "Loading..." 메시지가 먼저 뜨고 사라지면서 TodoList 목록의 제목이 렌더링된 것을 확인할 수 있다.
Custom Hook을 만들어 별도의 재사용 가능한 함수로 코드를 정리하자.
Side Effect에 해당하는 로직을 App.js에서 분리하는 작업을 해야한다.
App 함수 밖에 useFetch라는 커스텀 훅을 만든다.
이 함수는 비동기적으로 TodoList 데이터를 가져오고, loading 상태를 반환한다.
이 함수 안에서는 3가지의 작업을 처리한다.
이 일들을 수행하기 위해서는 url주소와 todos 상태를 업데이트할 수 있는 setTodos 함수가 필요하다.
따라서 useFetch의 인자로 url 주소와 setTodos 함수를 전달한 것이다.
App.js
// App.js
import React, { useEffect, useState } from 'react';
import './App.css';
// components
import List from './components/List.jsx';
// Custom Hook
const useFetch = (callback, url) => {
// loading data
const [loading, setLoading] = useState(false);
// todos 데이터 가져오기
const fetchInitialData = async () => {
setLoading(true); // loading 상태 관리
const response = await fetch(url);
const initialData = await response.json();
// 변경사항 todos 업데이트 시키기
callback(initialData);
setLoading(false); // loading 상태 관리
}
useEffect(()=>{
fetchInitialData();
}, []);
return loading;
}
// App Component
function App() {
// 등록한 todo들을 담은 배열
const [todos, setTodos] = useState([]);
// 새로운 todo
const [newTodo, setNewTodo] = useState();
const loading = useFetch(setTodos, 'http://localhost:4000/initialtodos');
// changeInputData : newTodo에 input에 입력한 내용을 저장하는 함수
const changeInputData = (e) => {
setNewTodo(e.target.value);
}
// addTodo : 새로운 todo를 배열에 추가하는 함수
const addTodo = (e) => {
e.preventDefault(); // 기본값 form 전송방지
setTodos([...todos, newTodo}]);
}
// 그냥 useEffect
useEffect(() => {
console.log("새로운 내용이 레더링됩니다.", todos);
}, [todos]);
return(
<>
<h1>Todo Application</h1>
<form action="">
<input type="text" name="" onChange={changeInputData}/>
<button onClick={addTodo}>ADD</button>
</form>
<List todos={todos} loading={loading}/>
</>
);
}
export default App;
현재까지의 코드는 새로운 할 일을 등록하는 과정에서 오류가 발생한다.
그 이유는 addTodo 함수에서 새로운 할 일을 Todo Array에 올바르지 않은 형식으로 추가했기 때문이다.
// App.js
// addTodo : 새로운 todo를 배열에 추가하는 함수
const addTodo = (e) => {
e.preventDefault(); // 기본값 form 전송방지
setTodos([...todos, {'title': newTodo, 'todoCode': todos.length, 'contents': '', done: false, edit: false}]);
}
}
useFetch.js
// useFetch.js
import React, { useEffect, useState } from 'react';
// Custom Hook
const useFetch = (callback, url) => {
// loading
const [loading, setLoading] = useState(false);
// ---- fetching Data X useEffect ---- //
// todolist 초기 데이터를 가져오는 함수
const fetchInitialData = async () => {
setLoading(true);
const response = await fetch(url);
const initialData = await response.json();
callback(initialData);
setLoading(false);
}
// - useEffect의 콜백함수에는 비동기 처리 로직을 직접 작성하면 X
// - 빈 deps 배열 []은 useEffect가 componentDidMount()처럼 한 번만 실행되는 것을 의미
useEffect(()=>{
fetchInitialData();
}, []);
return loading;
}
export default useFetch;
App.js
// App.js
import React, { useEffect, useState } from 'react';
import './App.css';
// custom Hook
import useFetch from './useFetch.js';
// components
import List from './components/List.jsx';
// App Component
function App() {
// ---- useState ---- //
// 등록한 todo들을 담은 배열
const [todos, setTodos] = useState([]);
// 새로운 todo
const [newTodo, setNewTodo] = useState();
const loading = useFetch(setTodos, 'http://localhost:4000/initialtodos');
// changeInputData : newTodo에 input에 입력한 내용을 저장하는 함수
const changeInputData = (e) => {
setNewTodo(e.target.value);
}
// addTodo : 새로운 todo를 배열에 추가하는 함수
const addTodo = (e) => {
e.preventDefault(); // 기본값 form 전송방지
setTodos([...todos, {'title': newTodo, 'todoCode': todos.length, 'contents': '', done: false, edit: false}]);
}
useEffect(() => {
console.log("새로운 내용이 추가되었습니다.", todos);
}, [todos]);
return(
<>
<h1>Todo Application</h1>
<form action="">
<input type="text" name="" onChange={changeInputData}/>
<button onClick={addTodo}>ADD</button>
</form>
<List todos={todos} loading={loading}/>
</>
);
}
export default App;
💡 참고
@ teo.log