Redux는 JavaScript 및 TypeScript 웹&앱의 상태 관리를 위한 라이브러리입니다. 애플리케이션의 모든 상태는 Redux store에 저장되며, 상태 변경을 유지하면서 action을 통해 Store의 상태를 업데이트합니다.
프로젝트를 시작하기 전에 React Redux
와 Redux Toolkit
을 설치해야합니다. Toolkit
은 Redux
작업을 단순화하고 Redux
앱을 만들기 쉽게 해준다고 합니다.
출처: Redux 시작하기
npm install react-redux @reduxjs/toolkit
우선 reducer.ts 파일을 생성하고 아래와 같은 코드를 작성합니다.
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
user: userSlice.reducer, // userSlice는 아직 정의 되지 않음
}
})
configureStore
함수는Redux Toolkit
에서 제공하는 함수로, Redux store를 생성하기 위해 사용됩니다. Redux 애플리케이션의 store는 state를 보유하고 state의 변화를 관리하는 저장소입니다.
이제 configureStore
함수 안에 작성한 userSlice를 정의해야 합니다.
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
const userSlice = createSlice({
name: 'user',
initialState: { uid: null },
reducers: {
setUser: (state, action) => {
state.uid = action.payload.uid;
},
logoutUser: (state) => {
state.uid = null;
}
}
});
Redux에서 action의 type 정의, action의 함수, reducer를 따로 작성해야 하는 번거로움이 있었다고 합니다. 하지만 toolkit에서 제공하는
createSlice
함수를 사용하면 이러한 번거로운 작업을 한 번에 정의할 수 있게 됩니다.
저는 Redux를 간단하게 사용하기 위해 Firebase로 로그인을 했을 때 존재하는 user의 uid 값만 정의했습니다.
state의 초기값을 initialState
로 설정할 수 있습니다. 이렇게 초기값을 설정하면 Redux store가 생성될 때 해당 state가 초기값으로 설정되며, action이 dispatch되면 초기값을 기반으로 state가 변경됩니다.
이때 문제점이 한 가지가 있는데, 타입스크립트를 사용하는 tsx 확장자를 사용 중이라, reducers
의 초기값인 initialState
의 type
과 action payload
의 type
을 설정해야합니다.
변경된 userSlice
는 다음과 같습니다.
interface UserState {
uid: string | null;
}
const userSlice = createSlice({
name: 'user',
initialState: { uid: null } as UserState,
reducers: {
setUser: (state, action: PayloadAction<UserState>) => {
state.uid = action.payload.uid;
},
logoutUser: (state) => {
state.uid = null;
}
}
});
initialState
와 action payload
의 type
을 선언하기 위해 UserState
라는 interface
를 생성하고 uid
의 값을 string | null
값으로 선언하였습니다.
이때 initialState
값은 as 키워드를 통해 타입을 단언했습니다.
PayloadAction
은Redux Toolkit
에서 제공하는 유용한 type 중 하나입니다.PayloadAction
은 action 객체의 payload에 대한 type을 정의하는 데 사용됩니다.
payload는 action에서 전달하고자 하는 데이터를 의미합니다. 그러나 Redux에서는 payload의 타입을 미리 정의하지 않으면, TypeScript에서 action과 관련된 type 오류를 확인하지 못 할 수가 있습니다.
변경된 전체 코드는 다음과 같습니다.
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';
interface UserState {
uid: string | null;
}
const userSlice = createSlice({
name: 'user',
initialState: { uid: null } as UserState,
reducers: {
setUser: (state, action: PayloadAction<UserState>) => {
state.uid = action.payload.uid;
},
logoutUser: (state) => {
state.uid = null;
}
}
});
export const { setUser, logoutUser } = userSlice.actions;
export const store = configureStore({
reducer: {
user: userSlice.reducer,
}
})
실제로 사용되는 변수는 setUser(로그인 시), logoutUser(로그아웃 시), store(Redux state 속성 전달)입니다.
이제 store 변수를 애플리케이션의 최상위인 index.tsx에 연결하여 Redux의 state가 작동할 수 있도록 해주어야 합니다.
import { Provider } from 'react-redux';
import { store } from './api/reducers';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
react-redux의 Provider를 불러와서 App component를 감싸줍니다. 그리고 이전에 작성한 store를 불러와서 위의 코드와 같이 연결해 줍니다.
Firebase Authentication을 참고하여 Firebase를 초기화해줍니다.
import { initializeApp } from "firebase/app";
export const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID
};
API KEY값은 보안이 중요하기 때문에 firebaseConfig
에 직접 작성하지 않고 .env.local
파일을 생성하여 KEY값을 연결하였습니다. React app에서 연결할 때는, REACT_APP_
으로 시작하는 환경 변수를 설정해줘야 합니다.
Firebase Google 로그인을 참고하여 Firebase의 Google 로그인을 구현합니다.
import React from 'react'
import LoginIcon from './LoginIcon';
import SecretLogo from './SecretLogo';
import { useDispatch } from 'react-redux';
import { setUser } from '../../api/reducers';
import { GithubAuthProvider, GoogleAuthProvider, getAuth, signInWithPopup } from 'firebase/auth';
import { initializeApp } from 'firebase/app';
import { firebaseConfig } from '../../api/firebase';
const LoginWrap = () => {
initializeApp(firebaseConfig);
const dispatch = useDispatch();
const GoogleLoginHandler = async () => {
const auth = getAuth();
const googleProvider = new GoogleAuthProvider();
try {
const result = await signInWithPopup(auth, googleProvider);
const uid = result.user.uid;
const userInfo = { uid: uid };
dispatch(setUser(userInfo));
} catch (error) {
console.error('Error message:', error);
}
}
return (
<div className='login_wrap'>
<SecretLogo />
<LoginIcon
text='구글로 시작하기'
GoogleLoginHandler={ GoogleLoginHandler } />
</div>
)
}
export default LoginWrap
Google 로그인을 구현하고나서 Redux store의 action을 dispatch 할 수 있는 useDispatch
를 호출하여 dispatch 변수에 할당합니다. signInWithPopup 함수가 정상적으로 실행되고나서, 이전에 만들어둔 reducers 함수인 setUser를 불러오고 Firebase의 uid값이 저장된 userInfo를 dispatch합니다.
Redux store의 state가 정상적으로 변경되었는지 확인하기 위해서는 Chorme 확장프로그램인 Redux DevTools를 설치하여 확장프로그램을 실행합니다.
store
의 uid
값이 initialState
에 설정해두었던 값인 null
로 잘 설정되어있습니다. 이제 구글 로그인이 구현된 버튼을 클릭하면 다음과 같이 State가 변경되는 것을 확인할 수 있습니다.
로그아웃도 이와같은 방법으로 구현하면 되며 로그아웃시에는 당연히 setUser
대신에 logoutUser
를 호출하여 구현해야합니다.
맨땅에 헤딩하면서 공부하려다 보니 어려운 것 같고..뭐든 그냥 해보자는 마인드로 시작하긴 했는데..이렇게 Redux를 사용하는게 맞나요..😭