심화 과정 명령어 모음
yarn create react-app 폴더이름
yarn add redux react-redux
yarn add json-server
yarn add axios
yarn add @reduxjs/toolkit
yarn add styled-components
yarn add react-query
yarn add lodash
yarn json-server --watch db.json --port 4000
리덕스 툴킷
리덕스를 쓰기 편하게 해주는 도구
리덕스와 구조, 패러다임이 같아서 새로운 것이 아니다.
모듈 파일 부분이 바뀐 것
yarn add @reduxjs/toolkit 설치
기존 counter 코드 활용
module에서 원래는
action value, action creator, reducer를 전부 직접 작성했지만
개발자들의 불만이 많아지자 리덕스 팀에서 이것들을 한번에 만들 수 있는 방법 제시
↓
redux toolkit 안에 있는 createSlice라는 API를 사용하게끔 권장
이것을 통하여 action creator와 reducer를 한 번에 생성할 수 있게 되었다.
상수로 선언해 놓으면 그 안에는 action creator와 reducer가 들어가게 된다.
modules 파일을 수정할 때 우리가 기존 파일에서 내보낸게 뭔지를 기억해야 한다.
→ action item을 만들어서 결국에 내보냈던 것은 action creator이다!
→ 결론적으로 export 한 것은 1. export action creator, 2. export reducer,
createSlice를 사용하면 1,2,3을 한꺼번에 할 수 있다.
createSlice는 객체를 인자로 받는다.
객체에는 name, initialState, reducer key가 있다.
redux toolkit의 장점! (2가지)
[immer] - 라이브러리
[devtools]
redux를 개발하면 state가 아주 다양하고 많이 변경되기 때문에 따라가기가 힘들다.
설치 방법 : chrome에 redux devtools 검색 후 확장 프로그램 추가
json-server
백앤드 서버 개발이 완료되기 전까지 프론트앤드가 테스트 할 수 있는 환경(간단한 db와 api서버 생성)을 제공하는 패키지
설치방법 : 터미널 실행 - yarn add json-server 명령어 실행 - 제일 바깥 폴더에 db.json 파일 생성 - 예시 코드 붙여넣기
json-server도 마찬가지로 moking API(fake API)이기 때문에 가짜 서버로서 돌아갈 수 있고 그렇기 때문에 다른 port를 지정해 주는게 좋다.
특정 port 지정 방법 : 터미널 실행하고 yarn json-server --watch db.json --port 4000
요청(Request) - 클라이언트
응답(Response) - 서버
Domain = Subdomain+Domain name
HTTP 메서드(요청의 종류)
· GET - 조회
· POST - 생성
· PUT, PETCH - 수정(변경)
· DELETE - 삭제
상태코드
클라이언트가 서버에 어떠한 요청을 하면 서버는 응답을 제공하는데 각 응답은 상태코드를 갖는다.
· 1xx(정보) : 요청을 받았으며 프로세스를 계속 진행합니다.
· 2xx(성공) : 요청을 성공적으로 받았으며 인식했고 수용하였습니다.
· 3xx(리다이렉션) : 요청 완료를 위해 추가 작업 조치가 필요합니다.
· 4xx(클라이언트 오류) : 요청의 문법이 잘못되었거나 요청을 처리할 수 없습니다.
· 5xx(서버 오류) : 서버가 명백히 유효한 요청에 대한 충족을 실패했습니다.
db.json파일에 입력한 부분은 새로운 port에 띄운다. → localhost로 띄우는건 맞지만 논리적으로 port가 다르기 때문에 다른 컴퓨터다!
즉, 서버와 클라이언트는 완전히 다른 물리적 공간에 있다는 개념.
서버와 클라이언트는 서로 약속한대로 데이터를 주고 받아야 한다!
컴포넌트가 처음에 렌더링이 됐을 때 비동기 함수가 실행되기 전에 return 부분 렌더링이 먼저 된다.
코드가 위에서부터 실행이 되지만 함수가 async 이기 때문에 함수가 돌아갈 때까지 return부분이 기다리지 않는 것
이럴 경우 하단의 todos는 없을 수도 있는, 즉 null일 수도 있기 때문에 '옵셔널 체이닝(?)'을 넣어주면 된다.
// ▶ 조회 함수
// 비동기 함수 - 서버 통신을 한다는 것 자체가 제어권이 나한테 없다는 것, 서버의 상태를 기다려야 함
const fetchTodos = async () => {
// 구조분해할당으로 의미있는 data만 뽑아왔다
const { data } = await axios.get("http://localhost:4001/todos");
console.log("data", data);
// 컴포넌트 내에서 state에 DB가 들어가게끔 setTodos
setTodos(data);
};
// ▶ 추가 함수
// submit을 눌렀을 때 post 요청을 하는 것이기 때문에 async 함수이다.
const onSubmitHandler = async () => {
axios.post("http://localhost:4001/todos", inputValue);
// 입력한 값을 보내고 나서 이것과 동일하게 state를 바꿔줘야지만 정상적으로 화면이 같이 렌더링된다.
// DB에는 아이디가 자동으로 입력이 되지만 state에는 아이디값을 알 수 없기 때문에 아이디 값이 자동으로 갱신되지 않는다.
// setTodos([...todos, inputValue]);
// 차라리 다시 DB를 읽어오는 방식이 더 적합할 수 있다. - 조회 함수 호출
fetchTodos();
};
// ▶ 삭제 함수
// axios 통신을 통해서 DB에 있는 아이템이 삭제 되어야 하기 때문에 async 함수이다.
const onDeleteButtonClickHandler = async (id) => {
// 몇 번째 아이템이 삭제되어야 하는지 알려줘야 하기 때문에 아이디를 받아왔다.
axios.delete(`http://localhost:4001/todos/${id}`);
// 실시간 반영을 위해 todos update를 위한 filter 함수
setTodos(
todos.filter((item) => {
return item.id !== id;
})
);
};
// ▶ 수정 함수
const onUpdateButtonClickHandler = async () => {
axios.patch(`http://localhost:4001/todos/${targetId}`, {
title: contents,
});
// 실시간 변경을 위해 map함수를 이용해서 새로운 배열 리턴
setTodos(
todos.map((item) => {
if (item.id == targetId) {
// 아이템은 그대로 뿌려주고 title만 입력한 contents 값으로 바꿔줘
return { ...item, title: contents };
} else {
return item;
}
})
);
};
[axios]★
별도 설치가 필요한 패키지
response나 request할 때 친절한 설명 및 데이터 구조 ○
더 많은 기능
----데이터 읽어오기 / 바로 then으로 response를 뽑아낼 수 있다.
const url = "https://jsonplaceholder.typicode.com/todos";
axios.get(url).then((response) => console.log(response.data));
----에러처리 / axios.get()요청이 반환하는 Promise 객체가 갖고있는 상태코드가 2xx의 범위를 넘어가면 거부(reject)한다.
const url = "https://jsonplaceholder.typicode.com/todos";
axios
.get(url)
.then((response) => console.log(response.data))
.catch((err) => {
console.log(err.message);
});
----axios 에러처리 예시코드
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`);
}
04. 비동기 통신 - axios, fetch 13
// 서버 오류
if (status === 500) {
console.log("Server error");
}
// 요청이 이루어졌으나 서버에서 응답이 없었을 경우
} else if (err.request) {
console.log("Error", err.message);
// 그 외 다른 에러
} else {
console.log("Error", err.message);
}
});
[fetch]
자바스크립트 내장 라이브러리
지원하지 않는 브라우저 존재
response나 request할 때 친절한 설명 및 데이터 구조 △
----데이터 읽어오기 / 데이터를 가져와도 then을 이용해서 json 변환이 필요하다
const url = "https://jsonplaceholder.typicode.com/todos";
fetch(url)
.then((response) => response.json())
.then(console.log);
----에러처리
fetch의 경우, catch()가 발생하는 경우는 오직 네트워크 장애 케이스입니다.
따라서 개발자가 일일히 then() 안에 모든 케이스에 대한 HTTP 에러 처리를 해야합니다.
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);
});
interceptor - HTTP에서 요청하고 응답을 할때 중간에서 뭔가를 가로채서 뭔가를 하는 개념
axios.get("http://localhost:4000,todos")
axios.post("http://localhost:4000,todos", todo)
axios.delete("http://localhost:4000,todos/${todoId}" )
위 처럼 모든 URL이 지정된 상태에서 port 번호가 바뀌어야 하는 상황이라면?
모든 URL을 전부 찾아서 바꿔줘야 한다. → 인적 리소스 낭비!
interceptor는 반복적으로 해야하는 어떠한 일들을 공통적으로 처리할 수 없을까 하는 고민에서 나온 개념
interceptor가 할 수 있는 일
* axios interceptor API와 관련된 로직을 넣는 부분을 src> axios> api.js 에 작성
리팩토링을 위한 작성 ↓
.env
REACT_APP_SERVER_URL=http://localhost:4001
api.js
import axios from "axios";
// axios instance를 만들어서 가공!
// axios.create() 새로운 인스턴스를 만들겠다! 인자로는 객체가 들어간다!
const instance = axios.create({
// 무슨 url을 달고 호출할 것인지
baseURL: process.env.REACT_APP_SERVER_URL,
});
export default instance;
App.jsx (수정)
--
import axios from "axios"; ❌
import api from "./axios/api"; ⭕
--조회 함수로 대표 예시
const { data } = await axios.get("http://localhost:4001/todos"); ❌
const { data } = await api.get("/todos"); ⭕
* 수정한 후에 서버 한 번 끊고 다시 yarn start 후 적용 확인
interceptor 적용을 위한 작성 ↓
api.js 추가작성
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);
}
);
위 함수에서 error를 발생시켜보기 위해 app.js에 추가 작성한 코드 ↓
const instance = axios.create({
// timeout이란 axios 콜을 했을 때 몇 초까지 기다릴지를 지정하는 것 (ms)
timeout: 1,
});
Thunk는 리덕스 환경에서 비동기 통신을 위해서 필요한 미들웨어이다!
미들웨어의 개념 ↓
리덕스에서 dispatch를 하면 action 이 리듀서로 전달이 되고, 리듀서는 새로운 state
를 반환했지만 미들웨어를 사용하면 이 과정 사이에 우리가 하고 싶은 작업들을 넣
어서 할 수 있다.
리덕스 미들웨어 사용 이유 : 서버와의 통신을 위해서 사용하는 것이 대부분이다.
가장 많이 사용되는 리덕스 미들웨어 : Thunk, saga 등
기존 : dispatch(액션객체)
Thunk : dispatch(함수) - 어떤 동작을 실행할 수 있게 해준다.
실습 순서 - memo.txt
1. thunk 함수 만들기 : createAsyncThunk
* reduxToolkit 내장 API / 따로 yarn add 같은거 안함
2. createSlice → extraReducer에 Thunk 등록
3. dispatch() → ()안에 액션 객체 말고 함수 넣기
4. test
작성 코드 ↓
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
// thunk 함수 생성 : createAsyncThunk
// ()안에 2개의 input
// (1) 이름 : 의미는 크게 없음, 나중에 액션 관련해서 이름 생성될 때 다시 확인
// (2) 함수
export const __addNumber = createAsyncThunk(
"ADD_NUMBER_WAIT",
// 비동기 함수 수행을 위해서 함수 작성 (이 함수도 인자 2개)
// (1) 컴포넌트에서 보내 줄 payload
// (2) Thunk 내장 기능을 가지고 있는 객체
(payload, thunkAPI) => {
// 수행하고 싶은 동작 : 3초를 기다리게 한다.
setTimeout(() => {
// thunkAPI.dispatch는 이전에 컴포넌트에서 dispatch를 호출한 것과 동일하다
thunkAPI.dispatch(addNumber(payload));
}, 3000);
}
);
todoSlice.js 작성 코드 ↓ (설명은 주석 확인)
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const initialState = {
todos: [],
isLoading: false,
isError: false,
error: null,
};
// 외부에서 response 까지는 데이터를 가지고 온 것
export const __getTodos = createAsyncThunk(
"getTodos",
// 서버랑 통신하는 부분이기 때문에 반드시 비동기 함수여야 한다.
async (payload, thunkAPI) => {
// 서버와의 통신은 항상 성공을 보장할 수 없기 때문에 try catch문으로 묶어준다.
try {
const response = await axios.get("http://localhost:4001/todos");
console.log("response", response.data);
// fulfillWithValue : toolkit에서 제공하는 API
// () 인자로는 서버로부터 가지고 온 데이터를 넣어주면 된다!
// Promise 객체가 resolve된 경우(네트워크 요청이 성공한 경우)에 성공한 value를 가지고 dispatch 해주는 기능을 가진 API
// = 위의 기능이 끝나고 나서 리듀서로 보내주는 기능!
// 이 경우 어디로 dispatch 해주는걸까 ? → extraReducers → __getTodos.fullfilled
return thunkAPI.fulfillWithValue(response.data);
} catch (error) {
console.log("error", error);
// rejectWithValue : toolkit에서 제공하는 API
// Promise 객체가 reject된 경우(네트워크 요청이 실패한 경우)에 실패한 value를 가지고 dispatch 해주는 기능을 가진 API
// 이 경우 어디로 dispatch 해주는걸까 ? → extraReducers → __getTodos.rejected
return thunkAPI.rejectWithValue(error);
}
}
);
export const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {},
// fulfillWithValue 와 rejectWithValue로 얻는 값이 상태에 따라서 자동으로 분류가 된다!
extraReducers: {
// 아직 통신이 진행 중일 때
[__getTodos.pending]: (state, action) => {
state.isLoading = true;
// 통신이 진행 중일땐 에러인지 모르니까 false
state.isError = false;
},
// 성공
[__getTodos.fulfilled]: (state, action) => {
state.isLoading = false;
state.isError = false;
// 성공의 경우에만 해주는 처리 - 서버로부터 받은 값을 넣어줘야 한다!
state.todos = action.payload;
},
// 오류가 났어도 끝나긴 끝난거니까 false가 되어야함
[__getTodos.rejected]: (state, action) => {
state.isLoading = false;
state.isError = true;
// 실패(에러가 난 경우)에만 처리 - rejectWithValue에 넣은 error 객체를 넣어준다.
state.error = action.payload;
},
},
});
export const {} = todosSlice.actions;
export default todosSlice.reducer;
App.jsx 에서 위의 코드 활용하여 데이터 보여주기 ↓
const App = () => {
const dispatch = useDispatch();
// extraReducer를 통해서 state에 isLoading, error, todos 관련 부분을 다 세팅해놓은 상태
// 아래처럼 작성하여 값을 가져올 수 있다!
const { isLoading, error, todos } = useSelector((state) => {
return state.todos;
});
useEffect(() => {
// 컴포넌트가 마운트 될 때만 이 함수를 호출할 수 있게 한다.
// 현재 payload는 필요하지 않다! axios.get하는 부분에 payload가 필요없기 때문!
dispatch(__getTodos());
}, []);
// 현재 상태가 isLoading 이면?
if (isLoading) {
return <div>로딩 중...</div>;
}
// 현재 error가 발생했다면?
if (error) {
return <div>{error.message}</div>;
}
// 현재 문제가 없으면 todos 정보를 뿌려준다.
return (
<div>
{todos.map((todo) => {
return <div key={todo.id}>{todo.title}</div>;
})}
</div>
);
};
⭐ 이러한 구조를 통해서 상태에 따라서 잘 걸러져서 보여지게끔 작성되었다!
로딩이 끝난 후에도 error가 난 경우라면 error.message를 띄우게 해서 todos를 보여주는
부분에 접근조차 할 수 없도록 하여 null & undefined 관련 오류도 없게 하였다!
예시 코드 - custom hook 적용 전 ↓
const App = () => {
const [name, setName] = useState("");
const [password, setPassword] = useState("");
const onChangeNameHandler = (e) => {
setName(e.target.value);
};
const onChangePasswordHandler = (e) => {
setPassword(e.target.value);
};
return (
<div>
<input type="text" value={name} onChange={onChangeNameHandler} />
<input
type="password"
value={password}
onChange={onChangePasswordHandler}
/>
</div>
);
};
위 코드에서 name과 password는 매우 비슷한 로직이 중복되면서 관리되고 있다.
이렇게 state를 관리하는데 state의 갯수가 매우 많아진다면?
반복되는 로직이나 코드를 custom hook으로 만들고 사용할 수 있다!
input과 관련된 코드이기 때문에 useInput이라는 custom hook을 만들고 적용 ↓
* src > hooks 폴더 생성 > useInput.js 파일 생성
useInput.js ↓
import { useState } from "react";
const useInput = () => {
// state
const [value, setValue] = useState("");
// handler
const handler = (e) => {
setValue(e.target.value);
};
// 위에서 만든 state와 handler를 배열로 return
// useInput을 사용하는 곳에서는 useInput() 하여 [value, handler]를 return 값으로 받는다.
return [value, handler];
};
export default useInput;
기존 미들웨어의 문제점 : 보일러 플레이트(많은 코드량), 규격화 문제(Redux는 비동기 전문 라이브러리 아님)
리액트 쿼리 장점 : 쉽다, 책임에서 자유롭다
주요 키워드 3개
(1) Query : 문의, 의문(ex 이 유저는 누구지?) → axios의 get 요청과 비슷하다.
(2) Mutation : 어떠한 데이터를 변경 → 추가, 수정, 삭제 (CUD)
(3) Query Invalidation : 1번의 Query를 무효화 시킨다. → 무효화 시킨 후 최신 상태로 쿼리를 가져온다!
todolist에 적용
▶ [Query]
App.jsx ↓
*프로젝트를 아우르는 상위 컴포넌트에서 react-query 관련 설정
const queryClient = new QueryClient();
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
);
};
*DB(서버)에 요청하는 axios에 관련된 로직들을 모아놓는 파일 생성
src > api 폴더 생성 > todos.js
todos.js ↓
// axios 요청이 들어가는 모든 모듈
import axios from "axios";
// 조회
const getTodos = async () => {
const response = await axios.get(`${process.env.REACT_APP_SERVER_URL}/todos`);
console.log(response.data);
return response.data;
};
export { getTodos };
TodoList.jsx ↓
* userQuery는 2개의 인자를 받는데 1개는 이름, 1개는 todos.js에서 만들어놓은 get함수이다
* isLoading isError 등을 따로 설정하지 않아도 미리 설정되어 있어서 매우 편리하다!
function TodoList({ isActive }) {
// 기존 redux 방법 - 사용x
// const todos = useSelector((state) => state.todos);
// useQuery의 첫번째 인자에는 query key가 들어간다! 여기서는 "todos"
// 이 query key는 queryClient.invalidateQueries에서 사용되기 때문에 중요하다!
const { isLoading, isError, data } = useQuery("todos", getTodos);
if (isLoading) {
return <h1>로딩중입니다....!</h1>;
}
if (isError) {
return <h1>오류가 발생했습니다....!</h1>;
}
return (
<StyledDiv>
<StyledTodoListHeader>
{isActive ? "해야 할 일 ⛱" : "완료한 일 ✅"}
</StyledTodoListHeader>
<StyledTodoListBox>
{/* 이제는 todos를 filter, map 하는게 아니고 가져온 data에 바로 적용 */}
{data
.filter((item) => item.isDone === !isActive)
.map((item) => {
return <Todo key={item.id} todo={item} isActive={isActive} />;
})}
</StyledTodoListBox>
</StyledDiv>
);
}
▶ [Mutation]
--글 작성 후 추가하는 로직 작성
todos.js ↓
// 추가 - Input 컴포넌트에서 사용
const addTodo = async (newTodo) => {
await axios.post(`${process.env.REACT_APP_SERVER_URL}/todos`, newTodo);
};
export { getTodos, addTodo };
Input.jsx ↓ / 길어서 추가 작성 부분만 표기
// import { addTodo } from "../../modules/todos"; ❌
import { addTodo } from "../../../api/todos"; ⭕
// ★ 리액트 쿼리 관련 코드
// new QueryClient()를 상위 컴포넌트(App.jsx)에서 해줬고
// 그렇기 때문에 여기서는 useQueryClient()를 해줘야한다!
// 그래야지 상위컴포넌트에서 만든 것을 이용해서 하나의 흐름으로서
// 쿼리 클라이언트를 사용할 수 있다! 짝꿍이라는 소리같음
const queryClient = useQueryClient();
// 뮤테이션 만드는 코드 ↓
// useMutation hook 사용
// 2개의 인자가 들어감 / 1. 만들어놓은 api 2. 성공, 실패에 대한 객체
const mutation = useMutation(addTodo, {
onSuccess: () => {
//
// queryClient.invalidateQueries("")
console.log("성공하였습니다!");
},
});
// form 태그 내부에서의 submit이 실행된 경우 호출되는 함수
const handleSubmitButtonClick = (event) => {
// dispatch(addTodo(newTodo)); ← ❌
// 인자 (api에 대한 인자, 여기서는 newTodo) ⭕
mutation.mutate(newTodo);
}
▶ Query Invalidation
* 위에서 작성한 기능을 이용하여 글 추가를 했을 경우 새로고침을 하지 않아도 화면에 바로 보여지게 하기
Input.jsx 추가 작성 ↓
const mutation = useMutation(addTodo, {
// 성공하면
onSuccess: () => {
// 무엇을 불러왔던 것을 다시 불러올 것인지
// 여기서 "todos"는 TodoList.jsx의 useQuery의 첫번째 인자인 query key이다!
// 이렇게 되면 "todos"로 불러온 data를 무효화 시키고 다시 불러오게 된다!
queryClient.invalidateQueries("todos");
console.log("성공하였습니다!");
},
});
* useQuery 인자
예시 코드 : const { isLoading, isError, data } = useQuery("todos", getTodos);
첫 번째 인자 "todos" : 쿼리의 키(Query Keys)
- refetching에 쓰인다. : invalidate
- 캐싱 처리에 사용
- 애플리케이션 전체에서 불러온 데이터를 공유하여 사용하는 키
- ★ 쿼리 키는 반드시 udique 해야 한다!
두 번째 인자 getTodos : 쿼리 함수(Query Functions)
- 비동기 함수이니까 당연히 promise 객체를 return 한다.
- promise 객체는 반드시 data를 resolve하거나 에러를 내어야 함.
원했던 상황이 아니여서 오류가 발생하면 axios, fetch, graphql중 적절한 방법을 이용하여 사용자가 오해하지 않도록!!! 오류 처리를 해줘야 함
* 습관이 되어야 하는 것 : try-catch, (비동기함수).then().catch() 로 적절한 상황을 제시해주기
* useMutation -hook, 함수, API
- mutation.mutate(인자)
인자는 반드시 한 개의 변수 또는 객체
결과를 객체로 갖고 있는다.
결과물 객체는 항상 isLdle(가동되지 않는, 놀고있는), isLoading, isError, isSuccess 상태 중 하나에 속해있다!
★ onSucess(성공)가 일어난 경우 onSucess가 필요한지 꼭 판단해야 한다!혹시나 갱신해줘야 하는 데이터가 있는지 반드시 생각하고 그 query key를 invalidate 해주는 습관이 필요하다.
개념 : 짧은 시간 간격으로 연속해서 이벤트 발생 시 과도한 이벤트 핸들러 호출을 방지하는 기법
[Throttling]
type 1. Leading Edge : 이벤트가 일어난 첫 번째에 function을 일으키겠다.
type 2. Trailing Edge : 이벤트가 일어났을 때 어느 정도 delay를 두고 마지막에 function을 일으키겠다.
type 3. Leading&Trailing Edge : delay의 처음과 마지막에 function을 일으키겠다.
* 발생한 이벤트들을 delay 단위로 그룹화하여 처음 또는 마지막 이벤트에 이벤트 핸들러 호출
사용 예시 : 무한스크롤
[Debouncing]
짧은 시간 간격으로 이벤트 발생 시 이벤트 핸들러를 호출하지 않다가 마지막 이벤트로부터 일정 시간 delay 후 한번만 호출
사용 예시 : 입력값 실시간 검색, 화면 resize 이벤트
CRA로 프로젝트 생성 → 메모리 누수 관련 내용 다루기 위해 react-router-dom 설치 (페이지 이동)
페이지 이동을 위한 App.jsx 작성 ↓
import Home from "./pages/Home";
import Company from "./pages/Company";
const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/company" element={<Company />} />
</Routes>
</BrowserRouter>
);
};
* setTimeout을 사용할 때는 항상 setTimeout에 대한 id가 필요하다! → 4
setTimeout은 항상 어떠한 값을 반환한다.
그리고 이 값이 바로 스로틀링과 디바운싱을 제어하는 키 = timerId가 된다!
Home.jsx에 작성된 Throttling 예제 코드 ↓
// 스로틀링과 디바운싱을 제어하는 키가 되는 timerId를 변수로서 변화시키며 다룰 것이기 때문에 let 변수에 담아준다!
let timerId = null;
// 반복적인 이벤트 이후, delay가 지나면 function
const debounce = (delay) => {
// timerId가 있으면
if (timerId) {
// 할당되어 있는 timerId에 해당하는 타이머를 제거하는 로직
// 기존에 있던 값을 제거하고 새로운 값이 된다!
clearTimeout(timerId);
}
timerId = setTimeout(() => {
console.log(`마지막 요청으로부터 ${delay}ms 지났으므로 API요청 실행`);
timerId = null;
}, delay);
};
<button onClick={() => debounce(2000)}>de BUTTON</button>
Home.jsx에 작성된 예제 코드 ↓
let timerId = null;
// 버튼을 얼마나 여러번 누르든지 상관없이 2초가 지나야 추가 요청을 1번만 받아줌
const throttle = (delay) => {
if (timerId) {
// timerId가 있으면 바로 함수 종료
return;
}
console.log(`API 요청 실행! ${delay}ms 동안 추가 요청은 안받음!`);
timerId = setTimeout(() => {
console.log(`${delay}ms 지남! 추가요청 받음!`);
timerId = null;
}, delay);
};
<button onClick={() => throttle(2000)}>th BUTTON</button>
Home.jsx 메모리 누수 발생 코드 작성 ↓
import { useNavigate } from "react-router-dom";
// 페이지 이동을 위한 navigate 생성
const navigate = useNavigate();
{/* 메모리 누수 구현을 위해 다른 페이지로 이동 */}
<div>
<button
onClick={() => {
navigate("/company");
}}
>
페이지 이동
</button>
</div>
* 위 코드처럼 작성될 경우 Home 페이지에서 th BUTTON을 누른 상태에서
* 페이지를 이동하여도 timer가 계속 동작하여 불필요하게 메모리를 점유하고 있다는 것을 알 수 있다!
Company 페이지로 넘어갔음에도 console창에는 2000ms 지남이라고 뜬다
Home.jsx 메모리 누수 해결 코드 ↓
= 컴포넌트가 사라질 때 timerId가 존재하여 어떤 동작이 실행되고 있다면 timerId를 없애줘라
// 메모리 누수 문제 해결
useEffect(()=>{
// 언마운트 시 어떠한 동작 가능 = Home 컴포넌트가 사라질 때
return () => {
//timerId가 존재한다면 어떠한 동작이 실행되고 있는 것!
if(timerId){
clearTimeout(timerId);
}
}
},[])
lodash 라이브러리 설치 - yarn add lodash
lodash는 왜 이름이 lodash인가?
표기법에 언더스코어가 들어감.
ex) _.debounce()
인증/인가의 수단 : 쿠키, 세션, 토큰
인증/인가의 차이점
[인증]
유저인지 확인 및 인증하기 위해 로그인 하는 것 - 등록된 회원인지 확인하는 절차
[인가]
유저에게 특정 리소스에 대한 접근을 인가(허가)해준다 - 권한 확인
--
http 프로토콜 통신의 특징 2가지
무상태 (Stateless) - 서버와 클라이언트 사이에 상태가 없다.
클라이언트가 서버에 요청하고 응답을 받는 한 사이클이 지나고나면 서버는 클라이언트의 상태에 대해 어떤 것도 기억하지 않는다.
그래서 매번 클라이언트는 서버에게 모든 상태 정보를 담아서 요청해야 한다.
→ 서버에 오는 요청이 많아서 서버의 스케일을 올려야 할 때 올리기 쉽다.
비연결성 (Connentionless) - 서버와 클라이언트는 연결되어 있지 않다.
서버 입장에서는 매번 새로운 요청이다. / 요청하고 응답해주면 끝. / 반대로는 채팅(계속 연결)
최소한의 서버 자원으로 서버 유지
--
[쿠키] - (브라우저가 주머니에 가지고 있는 쿠키)
- http 프로토콜 통신은 무상태와 비연결성 특징이 있음에도 불구하고 마치 서버가 클라이언트의 인증 상태를 기억하는 것처럼 구현하는 수단
- 무상태와 비연결성을 조금 개선하는 방향으로 쓰인다.
- 브라우저에 저장되는 텍스트 파일!!! key-value 형태
- 삭제, 유효기간 만료가 아닌 이상 자동으로 서버와 통신 시 주고 받는다.→ 인증상태 기억하는 것처럼
- 쿠키는 클라이언트에서 직접 추가/수정/삭제 할 수 있다 - 보안에 취약할 수 있다!
- 서버에 http 요청 할 때 마다 브라우저에 저장되어 있는 쿠키는 자동으로 서버에 보내집니다.
* (단, 동일한 Origin 또는 CORS를 허용하는 Origin에만 쿠키를 보냅니다.)
CORS의 O → Origin(출처) = http://localhost:3000
CORS란? Cross Origin Resource Sharing - 다른 출처(Origin)에 리소스를 허용하는 정책
[세션]
- 클라이언트(사용자)와 서버간의 연결이 활성화 된 상태 = 인증이 유지되고 있는 상태
로그인 성공 → 서버에서 세션 생성 후 저장 → 세션 키를 쿠키로써 브라우저에게 전달 → 브라우저가 세션 키를 가지고 있으면서 클라이언트가 요청할 시 브라우저에 있는 키를 서버에 자동으로 요청하여 연결성이 지속되는 것처럼 동작
-
로그인 회원가입시 세션 인증
인가 필요한 API 요청/응답
로그인 요청
axios.post(url, 보낼 data, config)
config → withCredentials: false // 기본값
withCredentials은 자격 증명을 사용하여 사이트 간 엑세스 제어 요청(CORS)을 해야 하는지 여부 (서버는 port4000)
withCredentials을 true로 해주어야 다른 서버 간의 정보를 교환할 수 있게 된다!
withCredentials: true 옵션이 클라이언트 쪽에 없으면 서버쪽에서 쿠키를 담아서 주더라도 브라우저에서는 쿠키에 담을 수 없다!
서버쪽에서 CORS 옵션을 주는 방법 → credentials: true
credentials: true 옵션이 없으면 로그인 요청시 Network에서 CORS 오류가 바로 나는 것을 알 수 있다!
토큰 - 클라이언트에서 보관하는 암호화된 인증 정보 / 세션은 서버에서 세션ID를 발급해서 서버에서 보관
→ 서버에서 보관할 필요가 없기 때문에 서버 부담을 줄여준다!
JWT : 웹에서 인증 수단으로 주로 사용된다.
JWT 특징
header.payload.signature 형식 - 3가지 데이터 구성
대략 과정 : 서버에서 발급 받은 JWT secret key - JWT 토큰 발급 - 브라우저로 넘겨준다.
* 암호화된 토큰을 누구나 복호화하여 payload를 볼 수 있습니다. → 토큰의 용도는 인증정보(payload)에 대한 보호가 아니라 위조 방지
secret key는 토큰이 유효한지 검증하는데 사용
★ JWT 토큰 인증 방식
로그인 회원가입시 토큰 인증
인가 필요한 API 요청/응답
-- Refresh Token (보안 강화)
Access Token(리소스 접근 인가를 받기 위해 사용되는 토큰)을 브라우저의 쿠키에 넣어놨는데 이것은 탈취 될 가능성이 있다.
이 경우를 대비해서 Refresh Token으로 보안을 강화할 수 있다.
→ Access Token만료 기간을 길게 잡고 인증상태를 오래 가져가면 서버 부담은 줄지만 탈취 당할 경우 보안 탈탈
해결방법 : 인증 보안 중요한 서비스 인증시 Access Token(기간 30분 정도)과 Refresh Token(기간 1~2주 정도) 2개의 토큰을 발급 → Access Token 만료 시 Refresh Token이 유효하면 새로운 Access Token 발급, Refresh Token 만료시 다시 로그인 하라는 메시지 응답
세션 인증 vs 토큰 인증