[ 비동기 통신 - axios, fetch ]
▶ Axios
Axios
공식문서에 따르면 axios 란 node.js와 브라우저를 위한 Promise 기반 http 클라이언트라고 소개하고 있음
다시 말해 http를 이용해서 서버와 통신하기 위해 사용하는 패키지라고 생각하면 됨
Axios 설치하기
터미널에서 패키지 설치
yarn add axios
▶ json-server 설정
API 서버는 json-server를 사용 한 프로젝트 내에서 3001 포트로 서버를 가동해 사용
테스트용 db.json 설정
{
"todos": [
{
"id": "1",
"title": "react"
}
]
}
▶ GET
Axios get
get은 서버의 데이터를 조회할 때 사용함
// url에는 서버의 url이 들어가고, config에는 기타 여러가지 설정을 추가할 수 있습니다.
// config는 axios 공식문서에서 확인하세요.
axios.get(url[, config]) // GET
사용하는 json-server API 명세서 확인하기
사용하는 json-server의 방식을 알아보아야 함
Axios는 GET 요청을 할 수 있도록 도와주는 패키지일뿐이지 요청을 해야하는 방식에 대한 확인은 사용할 API 명세서를 보아야 한다는 뜻 예를 들어 GET 요청을 할 때 path variable로 해야할지 query로 보내야할지는 API를 만든 사람이 하라는대로 해야 하기 때문 json-server의 공식문서를 보면 전체 정보나 상세 정보는 아래와 같이 path variable 로 url을 작성하면 됨 또한 filter와 같은 기능을 위해서 GET 요청을 하고자 할때는 query로 보내라고 명시되어 있음
코드로 알아보기
json-server에 있는 todos를 axios를 이용해서 fetching하고 useState를 통해서 관리하는 로직
// src/App.js
import React, { useEffect, useState } from "react";
import axios from "axios"; // axios import 합니다.
const App = () => {
const [todos, setTodos] = useState(null);
// axios를 통해서 get 요청을 하는 함수를 생성
// 비동기처리를 해야하므로 async/await 구문을 통해서 처리
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:3001/todos");
setTodos(data); // 서버로부터 fetching한 데이터를 useState의 state로 set
};
// 생성한 함수를 컴포넌트가 mount 됐을 떄 실행하기 위해 useEffect를 사용
useEffect(() => {
// effect 구문에 생성한 함수를 넣어 실행
fetchTodos();
}, []);
// data fetching이 정상적으로 되었는지 콘솔을 통해 확인
console.log(todos);
return <div>App</div>;
};
export default App;
▶ POST
Axios POST
axios.post(url[, data[, config]]) // POST
post는 보통 서버에 데이터를 추가할 때 사용 다만 post 요청에 대한 로직은 BE 개발자가 구현하는 것이기 때문에 추가외에 다른 용도로 사용될 수 있지만 보통은 클라이언트의 데이터를 body형태로 서버에 보내고자 할 때 사용
코드로 알아보기
GET 코드예시에서 POST 코드가 추가
// src/App.jsx
import React, { useEffect, useState } from "react";
import axios from "axios"; // axios import 합니다.
const App = () => {
// 새롭게 생성하는 todo를 관리하는 state
const [todo, setTodo] = useState({
title: "",
});
const [todos, setTodos] = useState(null);
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:3001/todos");
setTodos(data);
};
const onSubmitHandler = async(todo) => {
//1. 이때 todos는 [{투두하나}]임
await axios.post("http://localhost:3001/todos", todo); // 이때 서버에 있는 todos도 [{투두하나}]임
// 근데 여기서 서버 요청이 끝나고 서버는 [{투두가},{두개임}]
setTodos([...todos, todo]) 2. <-- 만약 이게 없다면, go to useEffect
//4. 새로고침해서 진짜 현재 서버 데이터를 받아오기전에 상태를 똑같이 동기시켜줌
//5. 어떻게보면 유저한테 서버에서 새로 받아온것처럼 속이는 것
};
useEffect(() => {
fetchTodos(); //3. 새로고침해서 여기를 다시 실행해줘야 서버값이 새로 들어옴 e.g) [{투두가},{두개임}]
}, []);
return (
<>
<form
onSubmit={(e) => {
// 👇 submit했을 때 브라우저의 새로고침을 방지
e.preventDefault();
onSubmitHandler(todo);
}}
>
<input
type="text"
onChange={(ev) => {
const { value } = ev.target;
setTodo({
...todo,
title: value,
});
}}
/>
<button>추가하기</button>
</form>
<div>
{todos?.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
</>
);
};
export default App;
POST 요청을 성공적으로 마친 후 브라우저를 새로고침하면 새롭게 추가한 Todo가 화면에 보임
네트워크탭 확인하기
post 요청을 보냈을 때 브라우저의 네트워크 탭에는 어떤 로그가 생기는지 확인
네트워크 쪽 개발을 할 때는 항상 브라우저에 있는 네트워크 탭을 확인하면서 개발을 진행해야 함
어떤 문제가 생겼을 때 이정보를 통해 디버깅을 할 수 있기 때문
- headers
Request URL을 통해서 URL로 post 요청을 보냈음을 알 수 있음
Request Method를 통해서 우리가 POST 메서드를 사용했음을 알 수 있음
Status Code를 통해서 201 코드를 받았고, 정상적으로 네트워크가 이루어졌음을 알 수 있음
status code는 자동으로 생성되는 것이 아니라 BE개발자가 직접 개발을 하고 설정한 code가 브라우저에게 보이게 됨
만약 BE개발자가 구현을 해놓지 않았다면 문맥과 다른 status code가 브라우저에 보일 수 있음
- payload
payload에서는 보낸 body를 확인 할 수 있음
- response
response에서는 우리가 보낸 post에 요청에 대한 서버의 응답값을 확인할 수 있음
이 Response 값은 자동으로 생성되는 것이 아니라 FE 개발자가 BE 개발자에게 요청한 것을 직접 개발을 해야 생기는 값
사용한 json-server의 경우 POST 요청을 했을 때 클라이언트가 보낸 body를 그대로 응답해주도록 만들어져 패키지이기 때문에 위와 같이 표시됨
▶ DELETE
Axios delete
DELETE는 저장되어 있는 데이터를 삭제하고자 요청을 보낼 때 사용
axios.delete(url[, config]) // DELETE
코드로 알아보기
GET, POST 와 함께 코드 작성
onClickDeleteButtonHandler 와 map을 돌린 항목별로 삭제하기 버튼을 추가
// src/App.jsx
import React, { useEffect, useState } from "react";
import axios from "axios";
const App = () => {
const [todo, setTodo] = useState({
title: "",
});
const [todos, setTodos] = useState(null);
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:3001/todos");
setTodos(data);
};
const onSubmitHandler = (todo) => {
axios.post("http://localhost:3001/todos", todo);
};
// 새롭게 추가한 삭제 버튼 이벤트 핸들러
const onClickDeleteButtonHandler = (todoId) => {
axios.delete(`http://localhost:3001/todos/${todoId}`);
};
useEffect(() => {
fetchTodos();
}, []);
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmitHandler(todo);
}}
>
<input
type="text"
onChange={(ev) => {
const { value } = ev.target;
setTodo({
...todo,
title: value,
});
}}
/>
<button>추가하기</button>
</form>
<div>
{todos?.map((todo) => (
<div key={todo.id}>
{todo.title}
{/* 디자인이 요상하긴 하지만..! 삭제 버튼 추가 */}
<button
type="button"
onClick={() => onClickDeleteButtonHandler(todo.id)}
>
삭제하기
</button>
</div>
))}
</div>
</>
);
};
export default App;
DELETE 요청이 성공적으로 이루어졌다면 브라우저를 새로고침 했을 때 삭제한 Todo가 화면에서 보이지 않음
▶ PATCH
Axios patch
patch는 보통 어떤 데이터를 수정하고자 서버에 요청을 보낼 때 사용하는 메서드
다만 http 환경에서 서로가 한 약속이자 문맥이기때문에 수정을 하고자 반드시 patch, put 을 써야만 하는 것은 아님
BE에 의해서 POST를 통해서 “수정" 이라는 기능은 충분히 만들 수 있기 때문
다만 이러한 약속들을 대부분의 개발자들이 지키고 있음
axios.patch(url[, data[, config]]) // PATCH
코드로 알아보기
GET, POST, DELETE 예제에 코드가 추가 put은 patch와 동일한 원리
Todo를 수정하기 위해 필요한 데이터는 2개가 있습니다. 수정하고자하는 Todo의 id 그리고 수정하고자 하는 값
수정하고자 하는 값은 기존에 있던 todo라는 state를 사용하면 될 것이고 예시코드에서는 id는 직접 입력을 해서 url로 넘겨주는 방식으로 구현했음 하지만 보통은 수정기능을 만들 때 직접 id를 입력받아 처리는 방식은 거의 없음
// src/App.jsx
import React, { useEffect, useState } from "react";
import axios from "axios";
const App = () => {
const [todo, setTodo] = useState({
title: "",
});
const [todos, setTodos] = useState(null);
// patch에서 사용할 id, 수정값의 state를 추가
const [targetId, setTargetId] = useState(null);
const [editTodo, setEditTodo] = useState({
title: "",
});
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:3001/todos");
setTodos(data);
};
const onSubmitHandler = (todo) => {
axios.post("http://localhost:3001/todos", todo);
};
const onClickDeleteButtonHandler = (todoId) => {
axios.delete(`http://localhost:3001/todos/${todoId}`);
};
// 수정버튼 이벤트 핸들러 추가 👇
const onClickEditButtonHandler = (todoId, edit) => {
axios.patch(`http://localhost:3001/todos/${todoId}`, edit);
};
useEffect(() => {
fetchTodos();
}, []);
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
onSubmitHandler(todo);
}}
>
{/* 👇 수정기능에 필요한 id, 수정값 input2개와 수정하기 버튼을 추가 */}
<div>
<input
type="text"
placeholder="수정하고싶은 Todo ID"
onChange={(ev) => {
setTargetId(ev.target.value);
}}
/>
<input
type="text"
placeholder="수정값 입력"
onChange={(ev) => {
setEditTodo({
...editTodo,
title: ev.target.value,
});
}}
/>
<button
// type='button' 을 추가해야 form의 영향에서 벗어남
type="button"
onClick={() => onClickEditButtonHandler(targetId, editTodo)}
>
수정하기
</button>
</div>
<input
type="text"
onChange={(ev) => {
const { value } = ev.target;
setTodo({
...todo,
title: value,
});
}}
/>
<button>추가하기</button>
</form>
<div>
{todos?.map((todo) => (
<div key={todo.id}>
{/* todo의 아이디를 화면에 표시 */}
{todo.id} :{todo.title}
<button
type="button"
onClick={() => onClickDeleteButtonHandler(todo.id)}
>
삭제하기
</button>
</div>
))}
</div>
</>
);
};
export default App;
▶ .env
env
env는 API key, port, DB 등 민감한 정보를 환경변수에 담아 관리하는 방법
env 주의사항
- root폴더에 생성해야함
- root 폴더에 env 파일이 위치하지 않으면 정상적으로 작동하지 않음
- 변수명은 반드시 'REACT_APP_'으로 시작해야 함
- 'REACT_APP_변수명' 형식이 아닐시 React가 인식하지 못하여 정상적으로 작동하지 않음
- process.env.REACT_APP_는 예약어임
- .gitignore에 env를 등록해야 함
- .gitignore에 등록해주어야 git에 등록할때 파일이 올라가지 않음
env 사용방법
- root 폴더에 .env 파일 생성
- 먼저 root폴더(package 파일과 같은 레벨)에 env 파일을 생성해줌
- 환경변수 입력
- key, value 형식으로 환경변수를 입력해줌
- 주의해야할 점은 key 앞에는 무조건 'REACT_APP_'을 붙여야 함
- 환경변수 불러오기
- 소스코드에서 import 없이 process.env.REACT_APP_변수명으로 불러오면 적용됨
env의 값을 변경하여 적용하고 싶다면 변경 후 server를 다시 실행시켜주어야 정상적으로 적용됨
▶ Fetch
개념
Fetch는 ES6부터 도입된 Javascript 내장 라이브러리
Promise기반 비동기 통신 라이브러리
axios처럼 데이터를 다루기 쉽고 내장 라이브러리이기 때문에 별도의 설치 및 import를 필요로 하지 않음
fetch의 단점
- 미지원 브라우저 존재
- 개발자에게 불친절한 response
- axios에 비해 부족한 기능
예시를 통해 보는 fetch와 axios의 차이
- CASE 1. 데이터 읽어오기
1. fetch
const url = "https://jsonplaceholder.typicode.com/todos";
fetch(url)
.then((response) => response.json())
.then(console.log);
- fetch().then을 한 상태여도 여전히 JSON 포맷의 응답이 아니기 때문에 response.json()을 한번 더 해주는 과정이 필요
- 따라서 fetch로 데이터를 요청하는 경우 두 개의 .then()이 필요함
2. axios
const url = "https://jsonplaceholder.typicode.com/todos";
axios.get(url).then((response) => console.log(response.data));
- axios는 친절하게도 응답(response)을 기본적으로 JSON 포맷으로 제공함
- 따라서 단순히 response.data로만 사용하면 됨
- CASE 2. 에러 처리
1. axios
const url = "https://jsonplaceholder.typicode.com/todos";
axios
.get(url)
.then((response) => console.log(response.data))
.catch((err) => {
console.log(err.message);
});
- axios.get()요청이 반환하는 Promise 객체가 갖고있는 상태코드가 2xx의 범위를 넘어가면 거부(reject)를 함
- 따라서 곧바로 catch() 부분을 통해 error handling이 가능
const url = "https://jsonplaceholder.typicode.com/todos";
// axios 요청 로직
axios
.get(url)
.then((response) => console.log(response.data))
.catch((err) => {
// 오류 객체 내의 response가 존재한다 = 서버가 오류 응답을 줌
if (err.response) {
const { status, config } = err.response;
// 없는 페이지
if (status === 404) {
console.log(`${config.url} not found`);
}
// 서버 오류
if (status === 500) {
console.log("Server error");
}
// 요청이 이루어졌으나 서버에서 응답이 없었을 경우
} else if (err.request) {
console.log("Error", err.message);
// 그 외 다른 에러
} else {
console.log("Error", err.message);
}
});
2. fetch
const url = "https://jsonplaceholder.typicode.com/todos";
fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(
`This is an HTTP error: The status is ${response.status}`
);
}
return response.json();
})
.then(console.log)
.catch((err) => {
console.log(err.message);
});
- fetch의 경우 catch()가 발생하는 경우는 오직 네트워크 장애 케이스임
- 따라서 개발자가 일일히 then() 안에 모든 케이스에 대한 HTTP 에러 처리를 해야함
[ axios 심화 - instance와 interceptor ]
▶ axios interceptor의 개념과 필요성
상황을 통해 살펴보는 interceptor의 필요성
axios.get("<http://localhost:3001/todos>");
axios.post("<http://localhost:3001/todos>", todo);
axios.delete(`http://localhost:3001/todos/${todoId}`);
며칠 후 어떠한 이유 때문에 호출하는 서버가 변경되었다면
- 변경 전 : http://localhost:3001
- 변경 후 : htto://localhost:4000
그러면 300군데를 모두 찾아서 변경해줘야 하고 엄청난 인적 리소스 낭비임
또한 매번 요청을 할때마다 console.log를 통해서 어떤 로깅을 하려고 한다면
console.log('요청 시작합니다...!');
코드를 넣어줘야 하고 이 또한 엄청난 인적 리소스 낭비
axios interceptor는 이름에서 알 수 있듯 앞에 두 상황에서 흐름을 가로채서 어떠한 코드 상의 관여를 할 수 있게함
- 요청(request)이 처리되기 전( = http request가 서버에 전달되기 전)
- 응답(response)의 then(=성공) 또는 catch(=실패)가 처리되기 전
위에서 했던 가정했던 상황들을 포함하여 요청 및 응답시에 필요한 작업들을 한꺼번에 처리할 수 있음
- 요청 헤더 추가
- 인증 관리
- 로그 관련 로직 삽입
- 에러 핸들링
이러한 부분에서 빛을 발함
▶ 실습
instance만들기, baseURL 설정하기
const data = axios.get("http://localhost:4000/");
custom 설정이 전혀 되어있지 않은 완전히 plain axios(순수 axios)
이 axios를 인스턴스(instance)라고 함
//src > axios > api.js
import axios from "axios";
// axios.create의 입력값으로 들어가는 객체는 configuration 객체
// https://axios-http.com/docs/req_config
// 위 주소를 참고
const instance = axios.create({
baseURL: "http://localhost:4000",
});
export default instance;
// App.jsx
import "./App.css";
import { useEffect } from "react";
import api from "./axios/api";
function App() {
useEffect(() => {
api
.get("/cafe")
.then((res) => {
console.log("결과 => ", res.data);
})
.catch((err) => {
console.log("오류가 발생하였습니다!");
});
}, []);
return <div>axios 예제입니다.</div>;
}
export default App;
get 요청하는 부분이 상당히 간결해졌음 이제는 서버의 정보가 변경 되어도 api.js 파일만 수정해주면 됨
request, response에 적용해보기
요청을 보낼 때 또는 서버로부터 응답을 받을 때(실패할 때) 특정한 일을 수행해야 한다면
// src > axios > api.js
import axios from "axios";
const instance = axios.create({
baseURL: "http://localhost:4000",
});
instance.interceptors.request.use(
function (config) {
// 요청을 보내기 전 수행
console.log("인터셉트 요청 성공!");
return config;
},
function (error) {
// 오류 요청을 보내기 전 수행
console.log("인터셉트 요청 오류!");
return Promise.reject(error);
}
);
instance.interceptors.response.use(
function (response) {
console.log("인터넵트 응답 받았어요!");
// 정상 응답
return response;
},
function (error) {
console.log("인터셉트 응답 못받았어요...ㅠㅠ");
return Promise.reject(error);
}
);
export default instance;
로그를 확인해보면 요청과 응답 중간에 가로채서 어떤한 작업을 수행해줌
실패 시켜보기
instance의 설정을 변경시켜서 요청을 실패한 예시
import axios from "axios";
// axios.create의 입력값으로 들어가는 객체는 configuration 객체
// https://axios-http.com/docs/req_config
// 위 주소를 참고
const instance = axios.create({
baseURL: "http://localhost:4000",
timeout: 1,
});
export default instance;
말도 안되는 짧은 시간이기 때문에 서버에서 응답을 받기 전에 오류를 냄
요청은 성공했지만 응답을 못받고 응답에 대한 로그로 타임아웃 1ms을 초과했다고 나옴
▶ 더 적용할 수 있는 부분
- 요청 시, content-type 적용
- token 등 인증 관련 로직 적용
- 서버 응답 코드에 대한 오류 처리(controller)
- 통신시작 및 종료에 대한 전역 상태를 관리하여 spinner, progress bar 등 구현 가능
axios interceptor를 통해 통신의 중간과정에서 개발자의 머릿속에 있는 모든 것을 다 구현할 수 있음
[ Thunk ]
▶ Redux 미들웨어
미들웨어
리덕스에서 dispatch를 하면 action 이 리듀서로 전달이 되고 리듀서는 새로운 state를 반환함
근데 미들웨어를 사용하면 이 과정 사이에 우리가 하고 싶은 작업들을 넣어서 할 수 있음
만약 counter 프로그램에서 더하기 버튼을 클릭했을 때 바로 +1를 더하지 않고 3초를 기다렸다가, +1이 되도록 구현하려면 미들웨어를 사용하지 않고서는 구현할 수 없음 왜냐하면 dispatch가 되자마자 바로 action이 리듀서로 달려가서 새로운 state를 반환해버리기 때문이죠. 즉 여기서 “3초를 기다리는 작업" 이 작업을 미들웨어가 해주는 것
보통 우리가 리덕스 미들웨어를 사용하는 이유는 서버와의 통신을 위해서 사용하는 것이 대부분이고 그 중에서도 많이 사용되고 있는 리덕스 미들웨어는 Redux-thunk 라는 것이 있음
▶ thunk
thunk 소개
리덕스 thunk란 리덕스에서 많이 사용하고 있는 미들웨어중에 하나
thunk를 사용하면 dispatch를 할때 객체가 아닌 함수를 dispatch 할 수 있게 해줌
즉 dispatch(객체) 가 아니라 dispatch(함수)를 할 수 있게 되는 것
그래서 중간에 하고자 하는 작업을 함수를 통해 넣을 수 있고 그것이 중간에 실행이 되는 것
그래서 아래 흐름과 같이 실행이 되는것 이 함수를 thunk 함수라고 부름
dispatch(함수) → 함수실행 → 함수안에서 dispatch(객체)
thunk 사용하기
아래 순서대로 구현을 할 것
- 우리의 첫 thunk 함수 만들기
- extraReducer에 thunk 등록하기
- dispatch(thunk 함수) 하기
- 테스트
첫 thunk 함수
thunk 함수의 역할은 “3초를 기다리는 것” 그리고 3초가 지나면 원래 하려고 했던 ADD_NUMBER를 해주는 것 까지가 thunk함수가 해야 할 일 툴킷에서는 createAsyncThunk 라는 API를 사용해서 thunk 함수를 생성할 수 있음 이 API는 함수인데 첫번째 인자에는 Action Value, 두번째 인자에는 함수가 들어감 이 함수에 하고 싶은 작업들을 구현하면 됨
두번째 들어가는 함수에서도 인자를 꺼낼 수 있는데 첫번째 인자(arg)는 thunk함수가 외부에서 사용되었을 때 넣은 값을 여기에서 조회할 수 있고 두번째 인자에서는 thnuk가 제공하는 여러가지 API 기능들이 담긴 객체를 꺼낼 수 있음
// thunk 함수는 createAsyncThunk 라는 툴킷 API를 사용해서 생성함
// __가 함수 이름에 붙는 이유는 이 함수가 thunk 함수라는 것을 표시하기 위한 개인의 convention
// 함수의 이름은 본인이 편한 이름으로 명명하면 됨
export const __addNumber = createAsyncThunk(
"ADD_NUMBER_WAIT",
(arg, thunkAPI)=>{},
);
3초를 기다리는 thunk 함수 첫번째 자리에는 action value를 넣고 두번째에는 함수를 넣음
함수안에는 setTimeout 라는 Web API를 이용해서 3초를 기다리게 했고 이후에 thunkAPI 안에 있는 dispatch를 통해서 우리가 원래 하려고 했던 addNumber라는 action creator를 넣었음
// src/redux/modules/counterSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const __addNumber = createAsyncThunk(
// 첫번째 인자 : action value
"addNumber",
// 두번째 인자 : 콜백함수
(payload, thunkAPI) => {
setTimeout(() => {
thunkAPI.dispatch(addNumber(payload));
}, 3000);
}
);
const initialState = {
number: 0,
};
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
addNumber: (state, action) => {
state.number = state.number + action.payload;
},
minusNumber: (state, action) => {
state.number = state.number - action.payload;
},
},
});
export const { addNumber, minusNumber } = counterSlice.actions;
export default counterSlice.reducer;
// src/App.jsx
import React from "react";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { minusNumber, __addNumber } from "./redux/modules/counterSlice";
const App = () => {
const dispatch = useDispatch();
const [number, setNumber] = useState(0);
const globalNumber = useSelector((state) => state.counter.number);
const onChangeHandler = (evnet) => {
const { value } = evnet.target;
setNumber(+value);
};
// thunk 함수를 디스패치한다. payload는 thunk함수에 넣어주면
// 리덕스 모듈에서 payload로 받을 수 있음
const onClickAddNumberHandler = () => {
dispatch(__addNumber(number));
};
const onClickMinusNumberHandler = () => {
dispatch(minusNumber(number));
};
return (
<div>
<div>{globalNumber}</div>
<input type="number" onChange={onChangeHandler} />
<button onClick={onClickAddNumberHandler}>더하기</button>
<button onClick={onClickMinusNumberHandler}>빼기</button>
</div>
);
};
export default App;
기존에는 addNumber 라는 action creator를 dispatch했다면 이제는 __addNumber 라는 thunk함수를 dispatch 해줌
[ 정리 ]
리덕스 미들웨어를 사용하면 액션이 리듀서로 전달되기전에 중간에 어떤 작업을 더 할 수 있음
Thunk를 사용하면 객체가 아닌 함수를 dispatch 할 수 있게 해줌[thunk의 핵심]
리덕스 툴킷에서 Thunk 함수를 생성할 때는 createAsyncThunk를 이용함
createAsyncThunk() 의 첫번째 자리에는 action value, 두번째에는 함수가 들어감
두번째로 들어가는 함수에서 2개의 인자를 꺼내 사용할 수 있는데
첫번째 인자는 컴포넌트에서 보내준 payload이고 두번째 인자는 thunk에서 제공하는 여러가지 기능
[ Thunk2 ]
▶ thunk에서 Promise 다루기
Todos 조회하기 기능 구현
json-server를 띄우고 Thunk 함수를 통해서 API를 호출하고 서버로부터 가져온 값을 Store에 dispatch 하는 기능
{
"todos": [
{
"id": 1,
"title": "hello world"
},
{
"id": 2,
"title": "hello React"
}
]
}
// src/redux/modules/todosSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
todos: [],
};
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;
// src/redux/config/configStore.js
import { configureStore } from "@reduxjs/toolkit";
import todos from "../modules/todosSlice";
const store = configureStore({
reducer: { todos: todos },
});
export default store;
구현 순서
- thunk함수를 구현 → __getTodos()
- 리듀서 로직 구현: reducers -> extraReducers
- extraReducers는 reducers에서 바로 구현되지 않는 기타 Reducer로직을 구현할 때 사용하는 기능
- 보통 thunk 함수를 사용할 때 extraReducers를 사용함
- 서버 통신: 100%로 성공하는 것이 아니기 때문에 서버와 통신을 실패했을 때도 어떻게 동작할지 구현해야함
- 지금까지의 redux state(todos, counter)
- 앞으로의 state(isLoading(통신중), isError(오류남), data)
- 기능확인(network)
- devTools 이용해서 작동 확인
- Store의 값을 조회 + 화면에 렌더링
▶ 구현하기
Thunk 함수 구현 → 서버에서 데이터 가져오기
initialState에 isLoading은 서버에서 todos를 가져오는 상태를 나타내는 값
초기값은 false이고 서버와 통신이 시작되면 true였다가 통신이 끝나면 다시 false로 변경됨
error는 만약 서버와의 통신이 실패한 경우 서버에서 보내주는 에러 메시지를 담아놓는 값
초기에는 에러가 없기때문에 null로 지정 대부분 서버와의 통신을 상태관리 할때는 data, isLoading, error로 관리
// src/redux/modules/todosSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
const initialState = {
todos: [],
isLoading: false,
error: null,
};
// 우리가 추가한 Thunk 함수
export const __getTodos = createAsyncThunk(
"getTodos",
(payload, thunkAPI) => {}
);
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
extraReducers: {},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;
const data는 Promise를 반환함 다시 말해 axios.get() (함수)은 Promise를 반환함
그래서 반환된 Promise의 fullfilled 또는 rejected된 것을 처리하기위해 async/await 을 추가
요청이 성공하는 경우에 실행되는 부분과 실패했을 때 실행되어야 하는 부분을 나누기 위해 try..catch 구문을 사용
// src/redux/modules/todosSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const initialState = {
todos: [],
isLoading: false,
error: null,
};
// 완성된 Thunk 함수
export const __getTodos = createAsyncThunk(
"todos/getTodos",
async (payload, thunkAPI) => {
try {
const data = await axios.get("http://localhost:3001/todos");
console.log(data);
} catch (error) {
console.log(error);
}
}
);
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
extraReducers: {},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;
1차적으로 Thunk 함수의 구현이 끝남 구현한 함수가 잘 작동하는지 1차적으로 한번 확인하기 위해 useEffect를 통해 App.js가 mount 됐을 때 thunk함수를 dispatch 하는 코드 작성
// src/App.jsx
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { __getTodos } from "./redux/modules/todosSlice";
const App = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(__getTodos());
}, [dispatch]);
return <div>App</div>;
};
export default App;
Thunk 함수 구현 → 가져온 데이터 Store로 dispatch 하기
fulfillWithValue는 툴킷에서 제공하는 API Promise에서 resolve된 경우 다시 말해 네트워크 요청이 성공한 경우에 dispatch 해주는 기능을 가진 API 인자로 payload를 넣어줄 수 있음
rejectWithValue도 툴킷에서 제공하는 API Promise가 reject 된 경우 네트워크 요청이 실패한 경우 dispatch 해주는 기능을 가진 API 마찬가지로 인자로 값을 넣을 수 있음 여기서는 catch에서 잡아주는 error 객체를 넣었음
// src/redux/modules/todosSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const initialState = {
todos: [],
isLoading: false,
error: null,
};
export const __getTodos = createAsyncThunk(
"todos/getTodos",
async (payload, thunkAPI) => {
try {
const data = await axios.get("http://localhost:3001/todos");
return thunkAPI.fulfillWithValue(data.data);
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
}
);
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
extraReducers: {},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;
리듀서 로직 구현 → extraRecuders
Slice 내부에 있는 extraRecuders 코드를 구현 extraRecuders에서는 pending, fulfilled, rejected에 대해 각각 어떻게 새로운 state를 반환할 것인지 구현할 수 있음 thunk 함수에서 thunkAPI.fulfillWithValue(data.data)라고 작성하면 [__getTodos.fulfilled] 이 부분으로 디스패치가 됨 그래서 action을 콘솔에 찍어보면 fulfillWithValue(data.data)가 보낸 액션객체를 볼 수 있음(type과 payload)
정리하면 원래는 action creator를 만들고 리듀서에서 스위치문을 통해서 구현해줘야 하는 부분을 모두 자동으로 해주고 있는 것
// src/redux/modules/todosSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const initialState = {
todos: [],
isLoading: false,
error: null,
};
export const __getTodos = createAsyncThunk(
"todos/getTodos",
async (payload, thunkAPI) => {
try {
const data = await axios.get("http://localhost:3001/todos");
return thunkAPI.fulfillWithValue(data.data);
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
}
);
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
extraReducers: {
[__getTodos.fulfilled]: (state, action) => {
console.log("fulfilled 상태", state, action); // Promise가 fullfilled일 때 dispatch
},
},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;
리듀서 로직을 구현 extraReducers에 pending와 rejected 상태에 따른 리듀서 로직을 추가로 구현해줌
// src/redux/modules/todosSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const initialState = {
todos: [],
isLoading: false,
error: null,
};
export const __getTodos = createAsyncThunk(
"todos/getTodos",
async (payload, thunkAPI) => {
try {
const data = await axios.get("http://localhost:3001/todos");
return thunkAPI.fulfillWithValue(data.data);
} catch (error) {
return thunkAPI.rejectWithValue(error);
}
}
);
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
extraReducers: {
[__getTodos.pending]: (state) => {
state.isLoading = true; // 네트워크 요청이 시작되면 로딩상태를 true로 변경함
},
[__getTodos.fulfilled]: (state, action) => {
state.isLoading = false; // 네트워크 요청이 끝났으니, false로 변경함
state.todos = action.payload; // Store에 있는 todos에 서버에서 가져온 todos를 넣음
},
[__getTodos.rejected]: (state, action) => {
state.isLoading = false; // 에러가 발생했지만, 네트워크 요청이 끝났으니, false로 변경함
state.error = action.payload; // catch 된 error 객체를 state.error에 넣음
},
},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;
기능확인
리덕스 devtools를 보면 만든 기능이 정상적으로 작동하고 있음을 알 수 있음
App.jsx가 mount됐을 때 Thunk함수가 dispatch되었고 Axios에 의해서 네트워크 요청이 시작됨
그래서 todos의 isLoading이 true로 변경된 것을 알 수 있음
네트워크 요청이 끝나고 성공하면 thunkAPI.fulfillWithValue(data.data);에 의해서 생성된 todos/getTodos/fulfillled 라는 액션이 dispatch가 되었고 그로 인해 리듀서에서 새로운 payload를 받아 todos를 업데이트 시킴 그리고 네트워크가 종료되었으니 isLoading상태도 false로 변경됨
Store 값 조회하고 화면에 렌더링 하기
모든 로직을 구현했으니 useSelector를 이용해서 store값을 조회하고 화면에 렌더링해보기
이 부분은 기존과 동일함 다만 각각의 상태에 따라 화면이 다르게 표시되어야 하는 부분이 추가되었음
서버에서 data를 가져오는 동안에는 우리의 서비스를 사용하는 유저에게 ‘로딩중' 임을 표시함 그리고 만약에 네트워크가 실패해서 정보를 가져오지 못한 경우 에러 메시지를 보여줌
// src/App.jsx
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { __getTodos } from "./redux/modules/todosSlice";
const App = () => {
const dispatch = useDispatch();
const { isLoading, error, todos } = useSelector((state) => state.todos);
useEffect(() => {
dispatch(__getTodos());
}, [dispatch]);
if (isLoading) {
return <div>로딩 중....</div>;
}
if (error) {
return <div>{error.message}</div>;
}
return (
<div>
{todos.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
);
};
export default App;
댓글