[React Hooks] Fetching Data & Custom Hooks

박은지·2022년 2월 20일
0

Todo Application

목록 보기
2/7

1. Server

React에서 데이터를 요청하면 요청한 데이터를 반환해주는 서버(Server) 개발환경을 구축해보자.

[1] root 디렉토리에 server 폴더를 만들고, 그 안에 server.js파일을 생성한다.

[2] 그 다음 코드에디터의 콘솔창에 아래의 명령을 입력하여 Express, cors, body-parser를 설치한다.

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값을 파싱하는 것이다.

[3] server.js

// 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}`);
});

2. Fetching Data

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;

3. 로딩 데이터

초기 데이터가 렌더링되기까지의 1~2초 동안 로딩데이터를 보여주도록 하자.

  • loading의 초기값은 false이다.
  • fetching Data를 가져오기 시작했을 때 true로 변한다.
  • TodoList데이터를 불러오고 난 후에는 다시 false로 바뀐다.

이 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 목록의 제목이 렌더링된 것을 확인할 수 있다.

4.

Custom Hook을 만들어 별도의 재사용 가능한 함수로 코드를 정리하자.
Side Effect에 해당하는 로직을 App.js에서 분리하는 작업을 해야한다.

App 함수 밖에 useFetch라는 커스텀 훅을 만든다.
이 함수는 비동기적으로 TodoList 데이터를 가져오고, loading 상태를 반환한다.
이 함수 안에서는 3가지의 작업을 처리한다.

  • loading 상태 관리하기
  • url에 따라 요청한 데이터 가져오기
  • 가져온 데이터를 이용하여 todos 업데이트 시키기

이 일들을 수행하기 위해서는 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;

5. addTodo 수정

현재까지의 코드는 새로운 할 일을 등록하는 과정에서 오류가 발생한다.
그 이유는 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}]);
  }
  }

6. useFetch 분리

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

0개의 댓글