우선 Context API
같은 경우 리액트 내부 기능- useContext
으로 존재하고 redux
는 외부라이브러리
이다.
컨텍스트 api
같은 경우 리덕스에 비해서 중첩되고 복잡해지는 컴포넌트
를 가지는 경우 훨씬 사용하기 부적절하다.
그 예시는 아래 코드를 비교해보면 간단히 느낄 수 있다.
// context api를 사용할 때 app의 구조
ReactDOM.render(
<React.StrictMode>
<AuthContextProvider>
<GoalContextProvider>
<App />
</GoalContextProvider>
</AuthContextProvider>
</React.StrictMode>,
document.getElementById("root")
);
// redux를 사용했을 때 app의 구조
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
현재 프로젝트에서 2개의 전역 상태값을 사용하기 때문에 그렇게 큰 차이는 느껴지지 않을지라도 redux
같은 경우 유일한 한 store
에 전역 상태값을 관리하기 때문에 하나의 store를 props를 가지는 Provider
로 앱을 감싸주면된다.
코드가 단순해져서 더욱 코드의 가독성이 향상된다.
반면에 ContextAPI
같은 경우 사용하는 모든 provider를 만들고
감싸주어야 하며 Provider간에도 계층
이 존재할수 있기 때문에 props 드릴링
에서 문제가 발생할 수 있다.
우선 redux는 위에 언급한대로 유일한 하나의 'store'
를 가져야 한다. 이 store에는 앱에 있는 모든 상태값
이 담기게 된다.
이 store를 component는 구독
하여 해당 상태값을 가져오게 된다.
각 컴포넌트에서 특정 상태값을 변경하고 싶다는 의사를 Action
에게 '전파'
한다. 직접적으로 state값을 변경하지 않기 위해서다.
변경요청을 전달받은 액션은 자신 안에 reducer
함수를 확인을 하고 해당 reducer 함수를 통해서 store
에 상태값을 업데이트
한다.
리덕스 툴킷은 리덕스를 사용하며 불편한 점들을 보완해주는 기능을 제공한다.
가장 우선적으로는 기존에 리덕스
같은 경우 아래와 같이 새로운 상태 값을 업데이트하려면 기존에 값을 모두 다시 적어주어 (또는 구조분해할당) 새로운 값으로 덮어씌우는 방식
으로 상태 업데이트를 진행을 한다.
const productReducer (state= initialState, action) => {
let {type, payload} = action;
switch(type){
case "GET_PRODUCT_SUCCESS":
return {...state, productList : payload.data};
case "GET_PRODUCT_SUCCESS":
return {...state, productList : payload.data};
default :
return {...state}
}
}
리덕스 툴킷은 반면 아래와 같이 표현이 가능하다. 여기서 중요한 점은 직접 변경을 하는 게 가능해보이지만 실제로는 리덕스 툴킷
내부적
으로 값을 복사
하여 새롭게 덮어씌우는 과정
이 이루어진다는 것이다.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {}
const productSlice = createSlice ({
name: "Product,
initialState,
reducers: {
getAllProducts(state.action) {
state.productList = action.payload.data
getSingleproduct(state,action) {
state.selectedItem = action.payload.data
}
})
참고 : https://www.youtube.com/watch?v=UKnLwVm9suY
// auth-context.js --- context API
import React, { useState } from "react";
const AuthContext = React.createContext({
token: "",
isLoggendIn: false,
login: (token) => {},
logout: () => {},
});
export const AuthContextProvider = (props) => {
const initialToken = localStorage.getItem("token");
const [token, setToken] = useState(initialToken);
const userIsLoggedIn = !!token;
const loginHandler = (token) => {
setToken(token);
localStorage.setItem("token", token);
};
const logoutHandler = () => {
setToken(null);
localStorage.removeItem("token");
};
const contextvalue = {
token: token,
isLoggendIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextvalue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
컨텍스트 API로 유저 authentication을 구현했을 때는 위 코드로 표현이 가능하다. 특징으로는 초기 상태값
에 함수
가 같이 포함되어 들어간다는 것이다.
그리고 내부적으로 다시 local state값인 token
을 설정해서 해당하는 값을 포함한 값인 contextvalue
를 다시 props
로 보낸다.
// store/index.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./auth";
import goalReducer from "./goal";
const store = configureStore({
reducer: { auth: authReducer, goal: goalReducer },
});
export default store;
redux에서는 각 액션에 대해서 별도의 파일로 관리가 가능하다. 그리고 각 액션이 담긴 reducer는 하나의 store로 통합
하여 관리한다.
// store/auth.js
import { createSlice } from "@reduxjs/toolkit";
const loggedIn = localStorage.getItem("token") ? true : false;
const token = localStorage.getItem("token")
? localStorage.getItem("token")
: "";
const initialAuthState = {
isAuthenticated: loggedIn,
token: token,
};
const authSlice = createSlice({
name: "authentication",
initialState: initialAuthState,
reducers: {
login(state, action) {
localStorage.setItem("token", action.payload);
state.isAuthenticated = true;
state.token = action.payload;
},
logout(state) {
localStorage.removeItem("token");
state.isAuthenticated = false;
state.token = "";
},
},
});
export const authActions = authSlice.actions;
export default authSlice.reducer;
초기상태값을 설정하는 부분은 비슷하다. 다만 액션을 설정할 때 각각을 함수로 정의하는 것이 아니라 reducer
안에 객체 형태
로 바뀔 상태값을 지정해준다. 그리고 함수를 상태값에 넣는 것이 아니라 실제 사용할 주체가 useSelector
를 통해서 조회
, usedispatch
를 통해서 변경
한다.
다음 카테고리에서 좀 더 자세히 알아보자.
이제 상태값을 조회 변경하는 경우 달라지는 코드를 정리해보자.
// 기존 Context api
// navabar - conditional rendering
import AuthContext from "../../store/auth-context";
import GoalContext from "../../store/goal-context";
const Navbar = ({ toggle }) => {
const authCtx = useContext(AuthContext);
const goalCtx = useContext(GoalContext);
const isLoggendIn = authCtx.isLoggendIn;
const logoutHandler = () => {
authCtx.logout();
goalCtx.reset();
};
작성한 context
를 가져와서 해당 컨텍스트를 각각 정의해준다.
그리고 해당하는 컨텍스트
안에서 상태값
과 함수
를 조회해준다.
import { authActions } from "../../store/auth";
import { goalActions } from "../../store/goal";
const Navbar = ({ toggle }) => {
const dispatch = useDispatch();
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
const logoutHandler = () => {
dispatch(authActions.logout());
dispatch(goalActions.reset());
};
const togglehome = () => {
scroll.scrollToTop();
};
useSelector : 상태값 조회
useDispatch : 해당하는 상태값 변경
위 두가지 훅을 이용해서 원하는 작업을 할 수 있다.
const getAllGoals = async () => {
// event.preventDefault();
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
await axios
.get("/goals", config)
.then((response) => {
if (response.data.success) {
dispatch(goalActions.update(response.data.data));
}
})// 이렇게 해당 액션에 인자로 payload를 넣어주면 된다.
.catch((err) => {
const statusCode = err.message.slice(-3, err.message.length);
console.log(`${statusCode} ${err.message}`);
});
};
dispatch 안에 액션에 인자로 값을 넣어주면 그 값은 action.payload
로 store로 전달이 된다.
const goalSlice = createSlice({
name: "goalList",
initialState: initialAuthState,
reducers: {
update(state, action) {
state.goals = action.payload;
},
reset(state) {
state.goals = [];
},
addItem(state, action) {
state.goals.push(action.payload);
},
},
});
update(state.action){state.goals = action.payload}
처럼 지정해주면 인자로 받은 값을 action.payload
로 사용할 수 있다.