Redux, 제대로 알고 있을까
다양한 곳에 동일한 정보가 분산되어 있을 때
한 곳의 정보를 업데이트하게 될 경우 props를 통해 전달해주는 방법은 데이터의 위치가 가깝다면 편리하지만 프로젝트 크기가 커진다면 번거로운 작업이 될 수 밖에 없다. 그렇다면 업데이트 사항을 props로 전달해주는 방법 말고 다른 방식은 뭐가 있을까? redux? context? mobx은 또 무엇일까
Redux - A predictable state container for JavaScript apps. | Redux
A predictable state container for JavaScript apps.
redux.js.org
리덕스 공식문서에 접속하면 더 다양한 정보를 살펴볼 수 있으며 리덕스를 사용하기 위해선 사전 설치 작업이 필요하다.
1. 터미널에서 리덕스 툴킷을 설치해준다.
- 리덕스를 사용하기 위한 프로젝트(리액트앱 또는 다른 프레임워크)를 이미 실행 중일 경우
yarn add @reduxjs/toolkit
- 프로젝트를 처음부터 시작하는 경우
리덕스와 함께 리액트앱을 동시에 설치를 원한다면 -- template를 통해 일괄 설치가 가능하다.
npx create-react-app my-app --template redux // Redux + JavaScript 프로젝트
npx create-react-app my-app --template redux-typescript // Redux + TypeScript 프로젝트
2. 오리지널 redux 라이브러리 설치를 진행한다.
yarn add redux
yarn add react-redux
이제 리덕스 사용 환경이 모두 설치되었다. 장착한 아이템을 실제로 사용해보기 위해 리덕스 활용 예시를 작성해보자. 로그인 확인 등에 가장 자주 사용되는 사용자 정보를 예시로 리덕스 코드를 작성해보았다.
3. 리덕스 코드 작성
Redux 폴더 내부에 'userSlice.js'인 Slice파일과 함께 Store를 생성해준다.
Store 내부에서는 상태( = 업데이트 되는 데이터 정보들의 각 상태 )가 관리되는 커다란 창고이고, slice는 창고에 저장된 각 한 슬라이스의 상태라고 생각하면 된다. 예를 들어 민수가 store 창고에 있는 보라색 감자칩 중 1개를 먹었다면, 나중에 펭수가 방문했을 때 창고의 상태를 확인하면 보라색 감자칩의 정확한 갯수를 확인할 수 있듯이 정보가 변화하는 상태를 관리해주는 편리한 관제탑 역할을 해준다.
앞에 2단계로 리덕스 설치를 진행했다면, 폴더 내부에 기본 환경으로 slice 와 store가 각각 counterSlice.js & store.js 이름으로 파일이 생성된 것을 확인할 수 있다.
1. src 폴더 > app 폴더 > store.js 파일 내부 초기 셋팅
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counterSlice';
export default configureStore({
reducer: {
counter: counterReducer,
},
});
2. src 폴더 > features 폴더 > counterSlice.js 파일 내부 초기 셋팅
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount));
}, 1000);
};
export const selectCount = state => state.counter.value;
export default counterSlice.reducer;
우리는 사용자 상태를 관리하는 user 관련 리덕스 코드를 작성할 것이기 때문에, 기본 숫자 카운트 관련 리덕스 코드가 작성된 counterSlice.js 파일을 -> userSlice.js로 명칭을 바꾸어 작업을 진행하면된다.
counterSlice에서 카운트 관련 데이터를 리덕스로 업데이트 / 관리 했다면, 각 슬라이스 이름에 맞게 userSlice 에서는 사용자 정보를 업데이트 및 관리를 수행한다.
하나의 동작에 대한 redux 경로 생성하는 방법 = 하나의 데이터 레이아웃을 만들어 가는 것과 같다.
1) store.js에 동작이름 + Slice(ex. emailSlice)로부터 counterReducer를 import 해주고,
reducer 객체 내부에 동작 이름 : counterReducer 정의해준다.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counterSlice'; -> 1.1. feature폴더에 목적에 맞는 Slice 생성해 sotre.js에 import 설정,
export default configureStore({
reducer: {
counter: counterReducer, -> 1.2. import한 slice의 counterReducer 정의해주기
},
});
2) feature 폴더 안에 해당 Slice.js 폴더를 만들어준다.
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({ -> 2.1 Slice 이름 변경
name: 'counter', -> 2.2 name의 'counter' 명칭 변경
initialState: {
value: 0, -> 2.3 초기값 설정해주기
},
reducers: { -> 2.4 reducers 정의 해주기 = 내가 할 작업
increment: state => { // mailopen 팝업
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const incrementAsync = amount => dispatch => {
setTimeout(() => {
dispatch(incrementByAmount(amount));
}, 1000);
};
export const selectCount = state => state.counter.value;
export default counterSlice.reducer;
import { createSlice } from '@reduxjs/toolkit';
export const mailSlice = createSlice({
name: 'mail',
initialState: {
selectedMail: null, -> 5. 선택된 메일도 container할 수있도록 초기값은 nll로 지정
sendMessageIsOpen: false, -> 0. 초기 상태 지정 ( 메시지 팝업 : False )
},
reducers: {
selectMail: (state, action) => {
state.selectedMail = action.payload;
},
openSendMessage: state => { -> 1. openSendMessage 액션을 수행할 경우 true로 변경해줘
state.sendMessageIsOpen = true;
},
closeSendMessage: state => { -> 2. closeSendMessage 정의하고 -> 전부 export 해줘야해
state.sendMessageIsOpen = false; 해당 액션 dispatch될 경우 sendMessageIsOpen값을 false로 체인지해준다
},
},
});
export const { selectMail, openSendMessage, closeSendMessage } = mailSlice.actions; -> 3. export 정의
export const selectOpenMail = (state) => state.mail.selectedMail;
export const selectsendMessageIsOpen = state => state.mail.sendMessageIsOpen; -> 4. select 만들어줘
export default mailSlice.reducer;
이제 저장된 값을 데이터에서 pull해오고 싶을땐 (value from the data),
select 코드를 작성하면 되는데 명칭 : 'select+초기값' ( ex. selectUser ) 형태로 작성해주면 된다.
state(data from the layout) => state.mail(name).초기값;
store에서 sendMessageIsOpen를 patch하기 위해서
App.js 에
const sendMessageIsOpen = useSelector(selectsendMessageIsOpen);
{sendMessageIsOpen && <SendMail />} -> 이런식으로 아래에 활용
const dispatch = useDispatch();
const user = useSelector(selectUser);
Compose btn 눌렀을때 액션 적용 시키고 싶은 컴포넌트 있는 곳 : Sidebar.js
import { useDispatch } from 'react-redux';
import { openSendMessage } from '../../features/mailSlice';
const dispatch = useDispatch();
<Button className="compose"
start={<Add fontSize="large"/>}
onClick={ () => dispatch( openSendMessage() )}>
Compose </Button>
Ex )
- SendMail.js
import { useDispatch } from 'react-redux';
import { closeSendMessage } from '../../features/mailSlice'; -> **동작 취할 액션 import
const dispatch = useDispatch();
<Close className="sendMail__closer" onClick={()=>dispatch(closeSendMessage())}/>
- EmailRow.js
const dispatch = useDispatch();
const openMail = () => {
dispatch(selectMail({
id, title, subject, description, time -> 이렇게 전달해주면 payload를 통해 다른 곳에서 데이터를 받아볼 수 있다 !
}));
history.push('/mail');
};
[기존]
<div onClick={() => history.push('/mail')} className="emailRow">
[변경]
<div onClick={openMail} className="emailRow">
payload 탑재된 데이터 실사용해보기 -> get 해오기!
Mail.js 에서 useSelector를 통해 데이터를 전달받아 사용할 수 있다.
import { useSelector } from 'react-redux';
import { selectOpenMail } from '../../features/mailSlice';
const selectedMail = useSelector(selectOpenMail);
<h2>{selectedMail?.subject}</h2> -> undefined 발생할 수 있으니 처리를 위한 것
<p>{selectedMail?.title}</p>
<p className="mail__time">{selectedMail?.time}</p>
<p>{selectedMail?.description}</p>
- userSlice.js 생성 해주기 - 로그인 위한 레이아웃
import { createSlice } from '@reduxjs/toolkit';
export const userSlice = createSlice({ // userSlice
name: 'user', -> name as user _ user로 변경해주고
initialState: {
user: null, -> beginig is null _ Default 값 설정해줘
},
reducers: { // 로그인과 로그아웃 액션
login: (state, action) => { -> login & login will have a action
state.user = action.payload; -> 해당 액션을 통해 user 의 값에는 null 대신 어떤 값이 탑제될 것
},
logout: state => { -> 로그아웃
state.user = null; -> reset the user(null) again
},
},
});
export const { login, logout } = userSlice.actions;
going to have a selector for all of this
export const selectUser = (state) => state.user.user; -> 모든 것을 위한 셀렉터 하나
go into the userSlice to get the user object
export default userSlice.reducer;
이렇게 생성한 userSlice는
1) go into my store and connect
import { configureStore } from '@reduxjs/toolkit';
import mailReducer from '../features/mailSlice';
import userReducer from '../features/userSlice'; // 1.
export default configureStore({
reducer: {
mail: mailReducer,
user: userReducer, // 2.
},
});
2) App.js 에서
grab the user -> const user 해줘 = useSelector를 이용하여 + import both of these things
const user = useSelector(selectUser);
3) 원하는 component에 - login.js에서 user 정보 dispatch해줘
- 버튼 클릭 시 취할 slice내부의 액션 import
- 해당 액션의 발생을 dispatch 할 { useDispatch }를 react-redux로 부터 import
- 컴포넌트 함수 내부에 동작 선언
const dispatch = useDispatch();
dispatch(login({
displayName : user.displayName,
email : user.email,
photoUrl : user.photoURL
}))
- 적용할 버튼에 함수 선언해주면 끝!
import { auth, provider } from '../firebase/firebase';
import { login } from '../../features/userSlice';
import { useDispatch } from 'react-redux';
function Login() {
const dispatch = useDispatch();
const signIn = () => {
auth
.signInWithPopup(provider)
.then(({user}) =>{
dispatch(login({
displayName : user.displayName,
email : user.email,
photoUrl : user.photoURL
}))
})
.catch(error => alert(error.message));
};
return (
<Button
variant="contained" color ="primary"
onClick={signIn}>Login</Button>
)
}
export default Login
const dispatch = useDispatch();
새로고침 할 경우 계정 로그인에서 벗어나는 현상은 useEffect hook를 사용해 곧장 해결할 수 있다.
- app.js
한번 로그인 했기때문에 존재하는 쿠키로 로그인이 이루어 진다.
import { auth } from './components/firebase/firebase';
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { login } from './features/userSlice';
function App() {
// const sendMessageIsOpen = useSelector(selectsendMessageIsOpen);
// const user = useSelector(selectUser);
const dispatch = useDispatch(); -> dispatch 선언
useEffect(() => {
auth.onAuthStateChanged(user => { -> firebase
if (user) { -> user 존재한다면
dispatch(
login({ -> login 액션 import -> 로그인 정보 payload해줘
displayName: user.displayName,
email: user.email,
photoUrl: user.photoURL
}))
}
})
}, [])
return (
<BrowserRouter>
{!user
? (<Login />)
: (
<div className="App"> </div>
)}
</BrowserRouter>
);
}
export default App;
프로젝트의 범위가 커지고 로그인 기능의 편리함을 위해서 우리는 어떤 방법을 선택해야 할지 고민해보자.

Posted by Ang