🔹 Redux-Toolkit-Todolist 최종 완료 깃헙 주소 : https://github.com/jennywoon/Redux-Toolkit-Todolist.git
✅ Redux Toolkit
1️⃣ 리덕스툴킷이란?
👌 리덕스툴킷
리덕스툴킷은 이전에 배운 리덕스를 개량한 것으로 생각하면 된다.
리덕스럴 사용하기 위해 작성했던 duck 패턴의 요소들이 전체적인 코드의 양을 늘린다는 개발자들의 불만이 발생하기 시작했고 리덕스 팀에서는 이것을 수용하여 코드는 더 적게, 그리고 리덕스를 더 편하게 쓰기 위한 기능들을 흡수해서 만든 것이 리덕스 툴킷이다. 줄여서 RTK 라고도 한다.
👌 새로운 것이 아니다
리덕스 툴킷은 배웠던 리덕스의 구조나 패러다임이 모두 똑같다. 즉, 새로운 것이 아니다.
리덕스의 전체 코드의 양을 줄이기 위해 새로운 API가 추가되었고 일일히 손으로 만들어 줘야 했던 ducks 패턴의 요소들이 어느정도 자동화 되었다.
또한, 컴포넌트에서 useSelector를 통해서 사용하는 것은 모두 똑같다. 바뀐 부분은 그저 모듈 파일 뿐이다.
2️⃣ 일반 리덕스와 코드 비교
👌 툴킷 설치하기
CRA를 통해 새로운 프로젝트를 생성하고 yarn을 통해 아래 패키지를 설치한다.
yarn add react-redux @reduxjs/toolkit
👌 count 프로그램 코드 비교
// Action Value
const ADD_NUMBER = "ADD_NUMBER";
const MINUS_NUMBER = "MINUS_NUMBER";
// Action Creator
export const addNumber = (payload) => {
return {
type: ADD_NUMBER,
payload,
};
};
export const minusNumber = (payload) => {
return {
type: MINUS_NUMBER,
payload,
};
};
// Initial State
const initialState = {
number: 0,
};
// Reducer
const counter = (state = initialState, action) => {
switch (action.type) {
case ADD_NUMBER:
return {
number: state.number + action.payload,
};
// [퀴즈 답]
case MINUS_NUMBER:
return {
number: state.number - action.payload,
};
default:
return state;
}
};
// export default reducer
export default counter;
🔹 리덕스 툴킷 사용했을 때
- Action Value와 Action Creator를 직접 생성해주지 않고 Action Value와 Action Creator, Reducer가 하나로 합쳐졌다.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
number: 0,
};
export 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;
▶️ 슬라이스는 createSlice라는 API를 통해 만들 수 있다. 그리고 그 인자로 설정정보를 객체로 받는데, 필수로 작성해줘야 하는 값은 name, initialState, reducer가 있다.
👌 configStore 비교
import { createStore } from "redux";
import { combineReducers } from "redux";
import counter from "../modules/counter";
const rootReducer = combineReducers({
counter,
});
const store = createStore(rootReducer);
export default store;
🔹 아래와 같이 reducer : 가 추가된다.
import { configureStore } from "@reduxjs/toolkit";
import counter from "../modules/counterSlice";
const store = configureStore({
reducers: {
counter,
},
});
export default store;
index.js Provider는 동일하게 진행된다.
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import store from "./redux/config/configStore";
import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
3️⃣ Redux Devtools 사용하기
현재 프로젝트의 state 상태라던가, 어떤 액션이 일어났을 때 그 액션이 무엇이고 그것으로 인해 state가 어떻게 변경되었는지 리덕스를 사용하여 개발할 때 아주 편리하게 사용할 수 있다.
▶️ Redux Devtools 설치 : https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko
👌 사용하는 법
1. 개발자 도구 탭 맨 오른쪽 >> 버튼 눌러서 redux 선택
2. State 선택 -> Raw 선택 -> 버튼 눌러서 작동 잘 되는지 확인하기
4️⃣ Flux 패턴
🔹 만화로 보는 Flux : https://bestalign.github.io/translation/cartoon-guide-to-flux/
🔹 Flux와 redux의 관계 : https://taegon.kim/archives/5288
5️⃣ 정리
- slice를 이용하면 Reducer, Action Value, Action Creator를 한번에 구현할 수 있다.
- 툴킷을 사용하면, 별도의 설정없이 devtools를 사용할 수 있다.
// 내장된 주요 패키지 : thunk, devtools, immer 등
✅ json - server
👌 꼭 알아야 하는 키워드
🔹 http 프로토콜 : https://jenny0520.tistory.com/85
🔹 비동기 시리즈 - 비동기개념 및 콜백함수 : https://jenny0520.tistory.com/86
🔹 비동기 시리즈 - Promise : https://jenny0520.tistory.com/87
🔹 비동기 시리즈 - async/await : https://jenny0520.tistory.com/88
1️⃣ json-server 란?
👌 json-server 정의 및 사용하는 이유
json-server 란, 아주 간단한 DB와 API 서버를 생성해주는 패키지이다. 우리가 json-server를 사용하는 이유은 Backend(이하 BE)에서 실제 DB와 API Server가 구축될 때까지 Frontend(이하 FE)개발에 임시적으로 사용할 mock data를 생성하기 위함이다.
json-server를 통해서 FE에서는 BE가 하고 있는 작업을 기다리지 않고 FE의 로직과 화면을 구현 할 수 있어 효율적으로 협업을 할 수 있다.
👌 json-server 설치하기
yarn add json-server
2️⃣ json-server 사용하기
👌 json-server 실행하기
json-server 가 간단한 패키지이긴 하나, 말 그대로 서버이다. 그래서 리액트와는 별개로 따로 실행을 해줘야 한다. 다시 말해 리액트토 start하고 json-server도 start 해야 한다. 그래야 리액트와 json-server가 서로 통신할 수 있다.
yarn json-server --watch db.json --port 3001
명령어의 대략적인 뜻은 db.json 이라는 것을 db로 삼고 3001포트에서 서버를 시작하겠다는 뜻이다.
이렇게 명령어를 입력하면 db.json이 자동으로 생성된다. 이 json 파일을 db로 사용하는 것이다.
그리고 db.json 안에 posts, comments, profile 이라는 기본적인 값들을 넘겨주었다.
왼쪽에는 yarn start 해주었고 오른쪽에는 위에 명령어를 입력해 json-server도 켜두었다.
👌 db.json 수정하고 브라우저에서 확인하기
db.json에 기존에 있던 내용을 없애고 테스트용으로 아래 내용 넣기
{
"todos": [
{
"id": 1,
"title": "json-server",
"content": "json-server를 배워봅시다."
}
]
}
만든 API 서버가 잘 작동하고 있는지 확인하기 위해 브라우저에서 http://localhost:3001/todos 로 이동하면
브라우저에서 위 정보들이 보이고 visual studio code 터미널에선
맨 아래에 GET / todos 200~~ 이렇게 누군가 GET을 했다고 알려준다.
3️⃣ Server 코드(backend 없을때 선제적 API 서버 URL 만들기, Heroku)
프로젝트 최상위 디렉토리에 server라는 폴더를 생성하고 그 안에 index.js를 생성한다. 그리고 아래 내용 복사해서 붙여넣기. 만약, 폴더 안에 폴더를 만들었다면, 예를 들어 Redux-Toolkit-Todolist라는 최상위 디렉토리에 todolist라는 폴더에 코드를 작성했다면, json-server용 폴더를 최상위인 Redux-Toolkit-Todolist에다 별도로 만들어야 한다. (CRA 사용)
const jsonServer = require("json-server");
const path = require("path");
const server = jsonServer.create();
const router = jsonServer.router(path.resolve(__dirname + "/db.json"));
const middlewares = jsonServer.defaults({
static: path.resolve(__dirname + "/../build/"),
});
const port = process.env.PORT || 3001;
server.use(middlewares);
server.use(jsonServer.bodyParser);
server.use(router);
server.listen(port, () => {
console.log("JSON Server is running");
});
나같은 경우, 최상위 폴더 안에 todolist 폴더를 만들어 코드를 작성했었으므로, json-server 폴더를 CRA를 사용해서 하나 만들었고 json-server를 설치해주어야 한다.
yarn add json-server
db.json 생성되었으면 아래처럼 써주기
{
"todos": [
]
}
만약 최상위 폴더에 지금까지 코드를 작성했다면, 나처럼 json-server 용 CRA 를 만들필요 없이 그 폴더 안에 server 폴더를 만들어서 db.json과 index.js만 만들어주면 된다(이미 제이슨 설치한 폴더이기 때문에)
▶️ cross-env 설치하기
yarn add cross-env
▶️ package.json 수정하기
"scripts": {
"start": "node server",
"start:dev": "cross-env NODE_PATH=src react-scripts start",
"build": "cross-env NODE_PATH=src react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"heroku-postbuild": "cross-env NODE_PATH=src npm run build"
},
// 원래코드
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
▶️ 중간점검
yarn build
이후 Done이 나오면 정상 작동한 것
▶️ 설정한 json-server 실행시켜 보기
node server
JSON Server is running 이라는 문구가 나오면 문제 없는 것. localhost:3001/todos로도 접속해서 잘 나오는지 확인하기
▶️ heroku CLI 설치하기
🔹 링크 : https://devcenter.heroku.com/articles/heroku-cli
운영체제에 맞는 프로그램으로 설치해야 한다!
▶️ 설치 완료된지 확인하기
heroku -v
나는 이상하게 heroku가 정상 설치가 계속 안 되었던 걸로 기억하는데,,,, 구글링을 통해 해결했었었다. 역시 항상 블로그를 쓰면서 기록해야 한다. 그래야 다시 구글링을 안하지..ㅠㅠ
▶️ 회원가입하기
heroku login
위 내용을 터미널에 입력하고 엔터를 치면 자동으로 heroku 사이트가 실행된다. 그때, 회원가입을 하면 된다!
회원가입 완료 후 다시 heroku login이라는 명령어를 실행하고 브라우저가 실행되면, 로그인하면 된다.
▶️ 커밋하기
git init # 첫번째 명령
git add . # 두번째 명령
git commit -m "initial commit" # 세번째 명령
분할 터미널을 이용해서 하는 것이 좋다.
▶️ heroku 프로젝트 생성하기
heroku create <프로젝트명> # 프로젝트명을 공백으로 하면 이름이 자동으로 랜덤설정 된다.
heroku config:set NPM_CONFIG_PRODUCTION=false # devDependency 도 설치하게 설정
# default branch가 main으로 ref 오류가 발생하시는 분들은 main으로 push 하면 된다.
git push heroku master # 또는 git push heroku main
위 코드를 한줄씩 순서대로 작성하면 된다.
주의해야 할 것은 아래 git push heroku master라고 되어 있는데, 내가 main이면 git push heroku main 이라고 작성한다.
또 안 되면? git push heroku HEAD:main(또는 마스터)로 HEAD를 붙여주면 된다. 나는 HEAD를 붙이지 않으면 작동하지 않았다.
이렇게 되면, 프로젝트명/heroku.com 이라는 API 서버 URL이 터미널에 보여지고 나는 json-server에 todos: 이렇게 썼기 때문에 프로젝트명/heroku.com/todos 로 들어가면 json-server 쓰던 것과 동일한 API를 볼 수 있다.
선제적인 FE 개발을 위해 필요한 작업!
✅ Axios
1️⃣ Axios
공식문서에 따르면 Axios란 node.js와 브라우저를 위한 Promise 기반 http 클라이언트라고 소개하고 있다. 다시 말해 http를 이용해서 서버와 통신하기 위해 사용하는 패키지라고 생각하면 된다.
👌 Axios 설치하기
CRA를 통해서 새로운 프로젝트를 생성하고 터미널에 명령어를 입력해서 Axios를 설치한다.
yarn add axios
2️⃣ json-server 설정
👌 테스트용 db.json 설정
{
"todos": [
{
"id": "1",
"title": "react"
}
]
}
3️⃣ GET
👌 Axios get
get은 서버의 데이터를 조회할 때 사용한다. 기본적인 사용방법은 아래와 같다.
// url에는 서버의 url이 들어가고, config에는 기타 여러가지 설정을 추가할 수 있습니다.
// config는 axios 공식문서에서 확인하세요.
axios.get(url[, config]) // GET
* Axios config 공식문서 소개 링크 : https://axios-http.com/kr/docs/req_config
👌 json-server API 명세서 확인하기
Axios를 사용해서 GET 요청코드를 작성하기에 앞서, 어떤 방식으로 요청 해야할지는 사용하는 json-server의 방식을 알아보아야 한다.
다시 말해, Axios는 GET 요청을 할 수 있도록 도와주는 패키지일뿐이지, "어떻게 요청을 해야하지?"와 같은 방식에 대한 확인은 우리가 사용할 API 명세서를 보아야 한다는 뜻이다. 예를 들어 GET 요청을 할 때 path variable로 해야할지 query로 보내야할지는 API를 만든 사람이 하라는 대로 해야 하기 때문이다.
json-server의 공식 문서를 보면, 전체 정보나 상세 정보는 아래와 같이 path variable로 url을 작성하면 된다.
그리고 filter와 같은 기능을 위해서 GET 요청을 하고자 할 때는 query로 보내라고 명시하고 있다.
👌 코드로 알아보기
// 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); // App.js:16
return <div>App</div>;
};
export default App;
콘솔로 확인하면 정상적으로 서버에서 가져와서 state가 set 한 것을 확인할 수 있다.
4️⃣ POST
👌 Axios POST
axios.post(url[, data[, config]]) // POST
post는 보통 서버에 데이터를 추가할 때 사용한다. 다만, post 요청에 대한 로직은 BE 개발자가 구현하는 것이기 때문에 추가외에 다른 용도로 사용될 수 있지만, 보통은 클라이언트 데이터를 body 형태로 서버에 보내고자 할 때 사용한다.
* json-server POST 요청 확인 방식 : https://www.npmjs.com/package/json-server
👌 코드로 알아보기
GET 코드에 POST 코드가 추가된다.
아래 코드에 대한 설명이다.
- 화면에 인풋과 버튼이 있고, 인풋에 어떤 값을 넣고 버튼을 클릭했을 때 onSubmitHandler 이 실행된다.
- onSubmitHandler 함수의 목적은 todo를 body에 담아 서버로 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 = (todo) => {
axios.post("http://localhost:3001/todos", todo);
};
useEffect(() => {
fetchTodos();
}, []);
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 요청을 보냈을 때, 브라우저의 네트워크 탭에는 어떤 로그가 생기는지 확인해본다.
네트워크 쪽 개발을 할 때는 항상 브라우저에 있는 네트워크 탭을 확인하면서 개발을 진행해야 한다. 어떤 문제가 생겼을 때 이 정보를 통해 디버깅을 할 수 있기 때문이다.
🔹 headers
- Request URL을 통해서 의도한 URL로 포스팅 요청을 보냈음을 알 수 있다.
- Request Method를 통해서 POST 메서드를 사용했음을 알 수 있다.
- Status Code를 통해서 201 코드를 받았고 정상적으로 네트워크가 이루어졌음을 알 수 있다. status code는 자동적으로 생성되는 것이 아니라 BE 개발자가 직접 개발을 하고 설정하 code가 브라우저에게 보이게 된다. 만약 BE 개발자가 구현을 해놓지 않았다면 문맥과 다른 status code가 브라우저에 보일 수 있다.
- 그 밖에도 Request header와 Response headers 정보가 추가적으로 있다.
🔹 payload
- payload 에서는 보낸 body를 확인할 수 있다.
🔹 response
- response에서는 보낸 post 요청에 대한 서버의 응답값을 확인할 수 있다. 이 Response 값은 자동으로 생성되는 것이 아니라, FE 개발자가 BE 개발자에게 요청한 것을 직접 개발을 해야 생기는 값이다. json-server의 경우 POST 요청을 했을 때, 클라이언트가 보낸 body를 그대로 응답해주도록 만들어진 패키지이기 때문에 위와 같이 표시된다.
5️⃣ 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가 화면에서 보이지 않는 것을 알 수 있다.
👌 네트워크 탭 확인하기
6️⃣ 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;
👌 네트워크 탭 확인하기
✅ Thunk part 1
1️⃣ Redux 미들웨어
👌 미들웨어란?
리덕스에서 dispatch를 하면 action이 리듀서로 전달이 되고 리듀서는 새로운 state를 반환한다. 그런데 미들웨어를 사용하면 이 과정 사이에 하고 싶은 작업들을 넣어서 할 수 있따.
만약 counter 프로그램에서 더하기 버튼을 클릭했을 때 바로 +1을 더하지 않고 3초를 기다렸다가 +1이 되도록 구현하려면 미들웨어를 사용하지 않고서는 구현할 수 없다. 왜냐하면 dispatch가 되자마자 바로 action이 리듀서로 달려가서 새로운 state를 반환해버리기 때문이다. 즉, 여기서 "3초를 기다리는 작업" 이 작업을 미들웨어가 해주는 것이다.
보통 리덕스 미들웨어를 사용하는 이유는 서버와의 통신을 위해서 사용하는 것이 대부분이고 그 중에서도 많이 사용되고 있는 리덕스 미들웨어는 Redux-thunk라는 것이 있다.
2️⃣ thunk
👌 thunk 소개
리덕스 thunk란, 리덕스에서 많이 사용하고 있는 미들웨어 중에 하나이다. thunk를 사용하면 우리가 dispatch를 할때 객체가 아닌 함수를 dispatch 할 수 있게 해준다. 즉, dispatch(객체) 가 아니라 dispatch(함수)를 할 수 있게 되는 것이다.
그래서 중간에 하고자 하는 작업을 함수를 통해 넣을 수 있고 그것이 중간에 실행되는 것.
dispatch(함수) → 함수실행 → 함수안에서 dispatch(객체)
이 함수를 thunk 함수라고 부른다.
👌 thunk 사용하기
1. 첫 thunk 함수 만들기
2. extraReducer에 thunk 등록하기
3. dispatch(thunk 함수)하기
4. 테스트
👌 첫 thunk 함수
만들 thunk 함수의 역할은 "3초를 기다리는 것" 이다. 그리고 3초가 지나면 원래 하려고 했던 ADD_NUMBER를 해주는 것 까지가 thunk 함수가 해야 할 일이다.
툴킷에서는 createAsyncThunk 라는 API를 사용해서 thunk 함수를 생성할 수 있다. 이 API는 함수인데, 첫번째 인자에서는 Action Value, 두번째 인자에서는 함수가 들어간다. 이 함수에 하고 싶은 작업들을 구현하면 된다.
두번째 들어가는 함수에서도 인자를 꺼낼 수 있는데, 첫번째 인자(arg)는 이 thunk 함수가 외부에서 사용되었을 때 넣은 값을 여기에서 조회할 수 있고 두번째 인자에서는 thunk가 제공하는 여러가지 API 기능들이 담긴 객체를 꺼낼 수 있다.
export const __addNuber = createAsyncThunk(
"ADD_NUMBER_WAIT"
(arg, thunkAPI) => {},
);
// thunk 함수는 createAsyncThunk라는 툴킷 API를 사용해서 생성한다.
// __가 함수에 붙는 이유는 이 함수가 thunk 함수라는 것을 표시하기 위한 개인의 convention 이다.
아래 코드 처럼 setTimeout라는 Web API 를 이용해서 3초를 기다리게 했고 이후에 thunkAPI 안에 있는 dispatch를 통해서 원래 하려고 했던 addNumber 라는 action creator를 넣었다.
export const __addNuber = createAsyncThunk(
// 첫번째 인자 : action value
"addNumber",
// 두번째 인자 : 콜백 함수
(payload, thunkAPI) => {
setTimeout(() => {
thunkAPI.dispatch(addNumber(payload))
}, 3000)
}
);
3️⃣ 정리
- 리덕스 미들웨어를 사용하면, 액션이 리듀서로 전달되기 전에 중간에 어떤 작업을 더 할 수 있다.
- Thunk를 사용하면, 객체가 아닌 함수를 dispatch 할 수 있게 해준다[thunk의 핵심].
- 리덕스 툴킷에서 Thunk 함수를 생성할 때는 createAsyncThunk를 이용한다.
- createAsyncThunk()의 첫번째 자리에는 action value, 두번째에는 함수가 들어간다.
- 두번재로 들어가는 함수에서 2개의 인자를 꺼내 사용할 수 있는데 첫번째 인자는 컴포넌트에서 보내준 payload이고 두번째 인자는 thunk에서 제공하는 여러가지 기능이다.
🔹 dispatch : thunk 함수안에서 dispatch를 할 때 사용
🔹 getState : thunk 함수안에서 현재 리덕스 모듈의 state 값을 사용하고 싶을 때 사용
✅ Thunk part2
1️⃣ thunk에서 Promise 다루기
👌 Todos 조회하기 기능 구현
조금 더 실용적인 예시로 json-server를 띄우고 Thunk함수를 통해서 API를 호출하고 서버로부터 가져온 값을 Store에 dispatch하는 기능을 해본다.
1. json-server 설치 및 서버 가동(db.json)
{
"todos": []
}
2. Slice로 todos 모듈 추가 구현
3. configStore에 리듀서 추가
4. 구현 순서
▶️ thunk 함수 구현 -> __getTodos()
▶️ 리듀서 로직 구현
- extraReducers 사용 : reducers에서 바로 구현되지 않는 기타 Reducer 로직을 구현할 때 사용하는 기능. 보통 thunk 함수를 사용할 때 extraReducers를 사용한다.
- 통신 진행중, 실패, 성공에 대한 케이스를 모두 상태로 관리하는 로직을 구현한다. 서버와의 통신은 100% 성공하는 것이 아니다. 서버와 통신을 실패했을때도 서비스가 어떻게 동작할지 구현해야 한다. 또한 서버와의 통신은 과정이다. 그래서 서버와 통신을 진행하고 있는 진행중 상태일때 서비스가 어떻게 작동해야할지 마찬가지로 구현해야 한다.
▶️ 기능확인 : devtools 이용해서 작동확인
▶️ Store 값 조회하고 화면에 렌더링 하기
2️⃣ 구현하기
👌 Thunk 함수 구현 -> 서버에서 데이터 가져오기
🔹 initialState
isLoading은 서버에서 todos를 가져오는 상태를 나타내는 값. 초기값은 false이고 서버와 통신이 시작되면 true였다가 통신이 끝나면(성공 또는 실패) 다시 false로 변경된다.
error는 만약 서버와의 통신이 실패한 경우 서버에서 어떤 에러 메시지를 보내준다. 그것을 담아놓는 값이며, 초기에는 에러가 없기 때문에 null로 지정한다.
대부분 서버와의 통신을 상태관리 할때는 data, isLoading, error로 관리한다.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
const initialState = {
todos : [],
isLoading : false,
error : null,
}
// 추가한 thunk 함수
expor const __getTodos = createAsyncThunk(
"getTodos",
(payload, thunkAPI) => {}
)
export const todosSlice = createSlice({
name : "todos",
initialState,
reducers : {},
extraReducers : {}, // 새롭게 사용할 extraReducers 꺼내기
})
export default todosSlice.reducer;
thunk 함수를 아래와 같이 작성한다.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
const initialState = {
todos : [],
isLoading : false,
error : null,
}
// 추가한 thunk 함수
expor 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 : {}, // 새롭게 사용할 extraReducers 꺼내기
})
export const {} = todosSlice.actions;
export default todosSlice.reducer;
const data는 Promise를 반환한다.
다시 말해 axios.get()(함수)는 Promise를 반환한다. 그래서 반환된 Promise의 fullfilled 또는 rejected 된 것을 처리하기 위해 async/await 을 추가했다.
그리고 이 요청이 성공하는 경우에 실행되는 부분과 실패했을 때 실행되어야 하는 부분을 나누기 위해 try..catch 구문을 사용했다.
1차적으로 Thunk 함수의 구현이 끝났기 때문에 확인해보기 위해 App.jsx에 임시적으로 코드를 구현해보면
import React, {useEffect} from "react";
import { useDispatch } from "react-redux";
import { __getTodos } from "./redux/moduls/todosSlice";
const App = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(__getTodos(),[dispatch]);
})
return <div>App</div>
}
export default App;
useEffect를 통해 App.jsx가 mount 됐을 때 thunk 함수를 dispatch 하는 코드이다.
콘솔을 보면 json-server로부터 데이터를 가져온 것을 확인할 수 있고 db에 넣어준 값이 없기 때문에 빈 배열로 표시된다. 리덕스 devtools에도 dispatch된 action을 보여준다.
👌 Thunk 함수 구현 -> 가져온 데이터 Store로 dispatch 하기
fulfillWithValue는 툴킷에서 제공하는 API이다.
Promise에서 resolve된 경우, 다시 말해 네트워크 요청이 성공한 경우에 dispatch 해주는 기능을 가진 API이다. 그리고 인자로는 payload를 넣어줄 수 있다.
rejectWithValue도 툴킷에서 제공하는 API이다.
Promise가 reject된 경우, 네트워크 요청이 실패한 경우 dispatch 해주는 기능을 가진 API이다. 마찬가지로 인자로 어떤 값을 넣을 수 있다.
//src/redux/modules/todosSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
import axios from "axios"
const initialState = {};
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 : {},
extrReducers: {},
})
export const {} = todosSlice.actions;
export default todosSlice.reducer;
dispatch라는 것은 리듀서에게 action과 payload를 전달해주는 과정이기 때문에 리듀서를 구현해야 한다.
👌 리듀서 로직 구현 -> extraReducers
Slice 내부에 있는 extraReducers에서 코드를 구현한다. extraReducers에서는 pending, fulfilled, rejected에 대해 각각 어떻게 새로운 state를 반환할 것인지 구현할 수 있다.
thunk 함수에서 thunkAPI.fulfillWithValue(data.data) 라고 작성하면 [__getTodos.fulfilled] 이 부분으로 디스패치가 된다. 그래서 action을 콘솔에 찍어보면 fulfillWithValue(data.data)가 보낸 액션객체를 볼 수 있다. type과 payload가 있다.
원래 action creator를 만들고 리듀서에서 스위치문을 통해서 구현해줘야 하는 부분을 모두 자동으로 해주고 있는 모습이다.
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: {
// 네트워크 요청이 시작되면 로딩상태를 true로 변경한다.
[__getTodos.pending] : (state,action) => {
state.isLoading = true;
}]
// 네트워크 요청이 끝났으니 false로 변경한다.
// Store에 있는 todos에서 서버에서 가져온 todos를 넣는다.
[__getTodos.fulfilled] : (state,action) => {
state.isLoading = false;
state.todos = action.payload
}
// 에러가 발생했지만, 네트워크 요청이 끝났기 때문에 false로 변경한다.
// catch된 error 객체를 state.error에 넣는다.
[__getTodos.rejected] : (state,action) => {
state.isLoading = false;
state.error = action.payload
}
},
})
export const {} = todosSlice.actions;
export default todosSlice.reducer;
👌 기능 확인
리덕스 devtools를 보면 만든 기능이 정상적으로 작동하고 있음을 알 수 있다.
네트워크 요청이 끝났고 성공했다. 그래서 thunkAPI.fulfillWithValue(data.data) 에 의해서 생성된 todos/getTodos/fulfilled라는 액션이 dispatch 되었고 그로 인해서 리듀서에서 새로운 payload를 받아 todos를 업데이트 시켰다. 그리고 네트워크가 종료되었으니 isLoading 상태도 false로 변경되었다.
👌 Store 값 조회하고 화면에 렌더링 하기
useSelector를 이용해서 store 값을 조회하고 화면에 렌더링 하는 부분은 기존과 동일하다. 다만 각각의 상태에 따라 화면이 다르게 표시되어야 하는 부분이 추가되었다.
서버에서 data를 가져오는 동안에는 서비스를 사용하는 유저에게 "로딩중"임을 표시한다. 그리고 만약에 네트워크가 실패해서 정보를 가져오지 못한 경우 에러 메시지를 보여준다. 위 두가지가 모두 아닌 경우에는 서버에서 불러온 todos를 화면에 보여준다.
import React from "react";
import { useDispatch } from "react-redux";
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;
3️⃣ 정리
- thunk 함수는 Reducers 아닌 외부에서 작성한 것이므로 extraReducers를 사용해야 한다.
- thunkAPI를 이용해서 Promise를 다룰 수 있다.
- 서버에서 가져오는 데이터는 로딩상태, 성공, 실패로 나누어서 상태를 관리하고 컴포넌트 단에서도 이에 따라 다르게 조건부 렌더링을 한다.
✅ React Hooks : optimizing
1️⃣ memo()
memo()는 훅이 아니다. 하지만 useCllback 이나 useMemo를 사용하기 위해서는 반드시 알아야 할 개념이다.
Memo라는 함수는 컴포넌트의 불필요한 리렌더링을 하지 않도록 해주는 함수이다. 불필요한 리렌더링이란, 화면에서 변경되는 부분이 없음에도 불구하고 아래의 이유로 화면시 다시 렌더링이 되는 것을 말한다. 불필요한 렌더링을 줄이는 것으로 리액트 프로젝트의 부하를 줄이고 퍼포먼스 능력을 향상시킬 수 있다.
1. 부모 컴포넌트가 렌더링 된 경우
2. 컴포넌트의 state가 변경된 경우
3. 부모로부터 전달받은 props 값이 변경된 경우
👌 컴포넌트의 리렌더링 확인하는 법
//App.jsx
import React, { useState } from "react";
import Button from "./components/Button";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (e) => {
setValue(e.target.value);
};
return (
<div>
<input type="text" value={value} onChange={onChangeHandler} />
<Button />
</div>
);
};
export default App;
// components/Button.js
import React from "react";
const Button = () => {
console.log("리렌더링되고 있어요.");
return <button>버튼</button>;
};
export default Button;
input에 값을 넣으면 Button.js에 있는 "리렌더링되고 있어요" 라는 텍스트가 콘솔에 찍히는 것을 볼 수 있다. 이 텍스트가 콘솔에 찍히는 것이 Button.js 컴포넌트가 리렌더링을 하고 있다는 것을 의미한다.
"컴포넌트가 렌더링 된다는 것 -> 컴포넌트 함수가 실행된다는 것 -> 리렌더링 된다는 것 -> 컴포넌트 함수가 계속 실행된다는 것" 이기 때문이다.
👌 불필요한 리렌더링을 막는 방법
App.js가 리렌더링 되는 이유는 부모 컴포넌트가 렌더링되었기 때문이다. value라는 state가 onChange 될때마다 setState 되고 있기 때문에 리렌더링된다. 하지만 이것은 불필요한 리렌더링이 아니다. input에 값을 입력할때마다 입력한 값이 화면에 보여야 하는 것이 당연하기 때문이다.
여기서 불필요한 리렌더링은 Button.js에서 발생하고 있다. input value 의 변화와는 아무 관련이 없는 Button이 컴포넌트의 state가 변경된 경우를 이유로 계속 렌더링이 되고 있다.
이럴 때 memo()를 사용한다. export default memo(Button)으로 Button.js를 감싸주면 2번의 이유가 발생하더라도 Button.js는 리렌더링을 하지 않는다.
import React, {memo} from "react";
const Button = () => {
console.log("리렌더링되고 있어요.");
return <button>버튼<utton>
}
export default memo(button);
그러나 만약 Button.js를 부모에서 동작하게 하고자 어떤 함수를 props로 넘겨주면 다시 Button.js는 리렌더링을 하기 시작한다. 이것을 해결하는 방법이 필요하다!
2️⃣ useCallback
👌 Button.js가 다시 리렌더링을 하게 된 이유
// src/components/Button.js
import React, { memo } from "react";
const Button = ({ onClick }) => {
console.log("리렌더링되고 있어요.");
return <button onClick={onClick}>버튼</button>;
};
export default memo(Button);
memo()를 사용했음에도 불구하고 Button.js가 다시 리렌더링을 하게된 원인은 함수를 props로 전달 받았기 때문이다. 그렇게 전달받은 props가 "3번 부모로부터 전달받은 props 값이 변경된 경우"에 해당되도록 원인을 제공한다.
// src/App.jsx
import React, { useState } from "react";
import Button from "./components/Button";
const App = () => {
// App.js가 리렌더링 될때마다 재생성됨
const [value, setValue] = useState("");
// App.js가 리렌더링 될때마다 재생성됨
const onChangeHandler = (e) => {
setValue(e.target.value);
};
// App.js가 리렌더링 될때마다 재생성됨
const onClickHandler = () => {
console.log("hello button!");
};
return (
<div>
<input type="text" value={value} onChange={onChangeHandler} />
{/* 매번 재생성되는 함수를 props로 넘겨줌 -> Button.js 리렌더링 유발 */}
<Button onClick={onClickHandler} />
</div>
);
};
export default App;
👌 Button.js의 리렌더링을 막으려면?
Button.js가 리렌더링을 하는 이유는 App.js가 리렌더를 함에 따라 props로 전달하는 함수를 매번 재생성하고 있기 때문이다. 즉, Button.js의 리렌더링을 막으려면, APP.js가 리렌더링한다고 해도 함수를 매번 재생성하지 않게 하면 된다.리액트에서는 이러한 상황에서 사용할 수 있는 훅을 제공한다.
👌 useCallback 이란?
컴포넌트가 리렌더링되더라도 생성된 함수를 새로 만들지 않고 재사용하고자 할때 사용하는 훅이다. memo()를 사용했음에도 Button.js가 리렌더링 되고 있는 원인을 해결할 수 있는 훅이다.
import React, {useCallback, useState} from "react";
import Button from "./components/Button";
const App = () => {
const [value, setValue] = useState("")
const onChangeHandler = (e)=>{
setValue(e.target.value)
}
const onClickHandler = useCallback(()=> {
console.log("hello button!")
},[])
return(
<div>
<input type="text" value={value} onChange={onChangeHandler}>
<Button onClick={onClickHandler}/>
</div>
)
}
export default App;
// src/components/Button.jsx
import React, { memo } from "react";
const Button = ({ onClick }) => {
console.log("리렌더링되고 있어요.");
return <button onClick={onClick}>버튼</button>;
};
export default memo(Button);
useCallback() 안에서 첫번째 매개변수로 구현하고자 하는 함수가 들어오고 두번째 매개변수자리에는 의존성 배열이 들어온다. 의존성 배열의 역할은 useEffect와 비슷하다.
useCallback을 사용하면, 함수가 생성되고 나서 재생성되지 않는다. 하지만 상황에 따라서는 이 함수를 재생성해줘야 할 때가 있다. 그러한 경우 의존성배열에 값을 넣어주면 해당 값이 변경되었을 때 이 함수도 같이 재생성된다.
// src/App.jsx
import React, { useCallback, useState } from "react";
import Button from "./components/Button";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (e) => {
setValue(e.target.value);
};
const onClickHandler = useCallback(() => {
console.log("hello button!");
}, []);
return (
<div>
<input type="text" value={value} onChange={onChangeHandler} />
<Button onClick={onClickHandler} />
</div>
);
};
export default App;
// src/components/Button.jsx
import React, { memo } from "react";
const Button = ({ onClick }) => {
console.log("리렌더링되고 있어요.");
return <button onClick={onClick}>버튼</button>;
};
export default memo(Button);
useCallback()을 사용해서 memo()가 다시 작동할 수 있도록 개선해볼 수 있다. Button.js에 props로 보내고자 하는 함수를 useCallback으로 감싸고 넘겨준다.
3️⃣ useMemo
useMemo는 useCallback과 똑같은 기능을 하는 훅이다. 다만, 대상이 함수가 아니라 배열이나 객체와 같은 값일 때 사용한다. 사용원리와 방법은 모두 useCallback과 같다.
import React, { useState } from "react";
import { useMemo } from "react";
import List from "./components/List";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (e) => {
setValue(e.target.value);
};
const data = useMemo(() => {
return [
{
id: 1,
title: "react",
},
];
}, []);
return (
<div>
<input type="text" value={value} onChange={onChangeHandler} />
<List data={data} />
</div>
);
};
export default App;
// src/components/List.jsx
import React, { memo } from "react";
const List = ({ data }) => {
console.log("리렌더링되고 있어요.");
return (
<div>
{data.map((todo) => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
);
};
export default memo(List);
4️⃣ 주의사항
- memo(), useCallback, useMemo의 무분별한 사용은 오히려 퍼포먼스 성능에 악영향이 될 수 있다. 반드시 리렌더링이 필요한 컴포넌트나 값(함수, 배열, 객체)에는 memo(), useCallback, useMemo를 사용하는 것이 오히려 좋지 않다. 왜냐하면 memo(), useCallback, useMemo를 사용하는 것은 리액트에게 "렌더링 이후의 값과 전의 값을 비교해서 같으면 재생성하지마!" 라고 주문을 거는 것인데, 반드시 바뀌어야하는 값에 저 기능을 사용하면 굳이 비교하지 않아도 될 것들에도 리액트가 비교를 하기 때문이다.
그렇기 때문에 저 기능을 사용하기에 앞서, 불필요한 리렌더링이 맞는지, 개선할 수 있는 부분인지 확인하고 사용해야 한다.
5️⃣ 정리
- memo()를 사용하면 컴포넌트의 불필요한 리렌더링을 막을 수 있다. 다만, 컴포넌트의 props가 있는 경우 useCallback과 useMemo를 적절하게 사용하여 리렌더링의 원인이 되는 것을 모두 제거해줘야 정상적으로 작동한다.
- useCallback의 대상은 함수, useMemo의 대상은 객체나 배열과 같은 값이다. 원시타입 데이터는 useMemo를 사용하지 않아도 리렌더링하지 않는다.
✅ Custom hook
input이 많아지면 useState와 이벤트 핸들러도 같이 증가하고 그로 인해 코드의 중복이 생기게 된다. 리액트에서는 반복되는 로직이나 중복되는 코드를 우리만의 훅, 즉 커스텀 훅을 통해서 관리할 수 있다.
1️⃣ useInput 커스텀 훅
커스텀 훅을 만들때 이름은 마음대로 해도 상관 없으나, 파일의 이름 앞에 use라는 키워드를 붙여줘야 한다.
src 폴더에 보통 hooks라는 폴더를 생성해서 커스텀 훅들을 보관하는 식으로 개발자들이 디렉토리 구조를 설계한다.
👌 코드 구현하기
import React, { useState } from "react";
const useInput = () => {
const [value, setValue] = useState("");
const handler = (e) => {
setValue(e.target.value);
}
return [value, handler];
}
export default useInput;
// 실제 사용할 때
const [title, onChangeTitleHandler] = useInput();
✅ 리액트 배포하기(vercel)
1️⃣ Vercel 이란?
Vercel은 AWS와 같은 클라우드 컴퓨팅 서비스를 제공하는 회사이면서도, 서비스의 이름이기도 하다. 그리고 React의 Framework인 Next.js를 개발한 회사이기도 하다.
아주 쉽고, 간단하며 성능도 괜찮고 일정 수준까지는 무료이다. 복잡한 설정을 할 필요(Zero-config)도 없고 깃헙과 연동하고 소스를 push할 때 마다 새로운 소스코드가 자동으로 배포도 된다. 또한, SSL 설정도 자동으로 적용이 되며 custom domain도 쉽게 연결할 수 있다.
▶️ Vercel로 들어가서 깃허브 아이디를 이용하여 로그인하기
▶️ Overview 화면 이동되면, New project 버튼 클릭
▶️ Impot Git Repository 메뉴에 생성한 레포지토리(깃헙)가 보이고 그것을 import 해주기
▶️ project name 동일하게 적고 Deploy 클릭, Status가 ready이면 배포 완료
2️⃣ 환경변수 .env 설정할 때
👌 설정에서 Environment Variables UI 클릭
NAME에 .env에서 만든 REACT_APP~~ 이런 것을 넣고 VALUE에 API 주소 넣으면 된다.(헤로쿠로 만든 것이라던지)
위 사진처럼, 실제 API 주소가 있는 곳에 가서 const 000 = ~~ 이렇게 설정해주고 process.env.로 입력해주면 되는데, 위 사진만 보면 모르겠었다...엄청난 구글링..
맨 위에 만들었던 깃헙 올려두었으니 참고하자!
'Front-End, CS 스터디 > 항해99 - React 스터디' 카테고리의 다른 글
[React] .env(환경 변수 관리) 정리 (0) | 2022.08.06 |
---|---|
[리액트 React] 심화반 강의 1주차 정리 (0) | 2022.08.04 |
[리액트 React] 기초반 강의 5주차 정리 (0) | 2022.08.03 |
[리액트 React] 숙련 학습 자료 정리 (0) | 2022.07.29 |
[리액트 React] 입문 학습 자료 정리 (0) | 2022.07.28 |