RTK (Redux Toolkit)
Redux의 공식적인 도구 모음으로, 복잡한 상태 관리 로직을 간단하고 효율적으로 작성하도록 돕는다.
RTK 상태 관리 기본 흐름
1. createSlice로 상태와 리듀서 정의
- 상태의 초기값 (initialState) 정의
- 상태를 변경할 리듀서 함수 작성
- createSlice 호출로 액션과 리듀서 생성
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type CounterState = {
value: number;
};
const initialState: CounterState = { value: 0 };
const counterSlice = createSlice({
name: 'counter', // slice 이름
initialState,
reducers: {
increment(state) {
state.value += 1;
},
decrement(state) {
state.value -= 1;
},
setValue(state, action: PayloadAction<number>) {
state.value = action.payload;
},
},
});
export const { increment, decrement, setValue } = counterSlice.actions;
export default counterSlice.reducer;
2. Redux Store 설정
RTK의 configureStore를 사용하여 스토어 설정
1. reducers를 combineReducers 없이 바로 객체 형태로 전달
2. RTK의 기본 미들웨어 자동 설정
3. 추가 미들웨어 필요 시 옵션으로 전달
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice'; // 1번에서 만든 슬라이스 리듀서 가져오기
const store = configureStore({
reducer: {
counter: counterReducer, // 슬라이스 이름과 리듀서를 연결
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
3. React 컴포넌트에서 상태 사용
react-redux의 훅을 사용!
- useSelector: 스토어에서 상태를 읽음
- useDispatch: 액션을 디스패치하여 상태 변경
1. useSelector로 상태 읽기
2. useDispatch로 액션 호출
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from './store';
import { increment, decrement, setValue } from './counterSlice';
const Counter = () => {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(setValue(10))}>Set to 10</button>
</div>
);
};
export default Counter;
4. Redux Provider로 스토어 연결
1. Provider를 가져와 store를 전달
2. React 컴포넌트를 Provider로 감싸기
우선 userSlice.ts 파일을 만들어서 createSlice 먼저 작성해 주었다.
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export type UserState = { // Redux Store에서 관리할 user 상태의 타입
user: {
id: string;
email: string;
nickname: string;
gender: 'male' | 'female' | 'none';
birthday: string;
created_at: string;
updated_at: string;
} | null; // 로그아웃 또는 데이터가 없을 때
};
const initialState: UserState = {
user: {
id: '',
email: '',
nickname: '',
gender: 'none',
birthday: '1925-01-01',
created_at: '',
updated_at: '',
},
};
const userSlice = createSlice({
name: 'user', // 슬라이스 이름
initialState,
reducers: { // 상태 변경 함수
setUser(state, action: PayloadAction<{ user: UserState['user'] }>) {
// 전달된 데이터를 state.user에 저장
console.log('setUser 액션 호출:', action.payload); // 확인용
state.user = action.payload.user;
},
clearUser(state) { // 사용자 데이터 초기화
state.user = null;
},
},
});
export const { setUser, clearUser } = userSlice.actions; // 액션 내보내기
export default userSlice.reducer; // Redux Store에서 사용할 리듀서
RTK에서 createSlice의 리듀서 함수는 두 가지 주요 매개변수를 받는다.
1. state : 현재 상태
- 리듀서 함수는 state를 읽거나 변경하여 새로운 상태를 만듦!
2. action.payload : 액션에서 전달된 데이터
- 액션을 호출할 때 전달되는 데이터. (dispatch(setUser(payload))
- payload는 상태를 변경하는 데 필요한 정보를 담고 있다.
dispatch(setUser({ user:
{
id: '123',
email: 'test@example.com',
nickname: 'John',
gender: 'male',
birthday: '1990-01-01',
created_at: '2025-01-01',
updated_at: '2025-01-07'
}
}));
예를 들어 이렇게 setUser 액션을 호출하면,
action.payload는 아래 데이터를 담고 있는 것.
{
user: {
id: '123',
email: 'test@example.com',
nickname: 'John',
gender: 'male',
birthday: '1990-01-01',
created_at: '2025-01-01',
updated_at: '2025-01-07'
}
}
createSlice를 작성했으니, 다음은 configureStore을 작성해야 한다.
기존에 있는 store.ts 파일의 reducer 부분에
user: userReducer;
을 추가해주면 끝.
이미 Provider로 감싸져 있기 때문에 코드는 생략하겠다.
로그인 컴포넌트로 넘어가기 전에 로그인 API 호출, 그리고 유저 정보 API 호출 부분을 RTK Query로 구현해보자.
// userApi.ts
login: builder.mutation({
query: newUser => ({
url: '/user/login', // 로그인 엔드포인트
method: 'POST',
body: newUser, // 요청 데이터
headers: {
'Content-Type': 'application/json',
},
}),
}),
getUserInfo: builder.mutation({
query: () => ({
url: '/user/me', // 회원정보 받아오기 엔드포인트
method: 'GET',
}),
}),
회원가입 때 작성해 놓은 것들이 있어서 조금 수월하게 작성했다.
login의 경우 데이터를 body에 담아서 POST 요청을 보내주고,
getUserInfo의 경우는 GET 요청을 보내준다.
근데 GET 요청에는 원래 builder.mutation이 아닌 builder.query를 사용하는 것이 맞으나
나는 페이지 로드 시가 아니라 로그인 성공 시에 유저 정보를 받아와야 해서... 이런 조건이 붙은 경우는 mutation을 사용하기도 한다고 한다.
(아닌가...)
아무튼 이렇게 작성해주고, store.ts에 리듀서 연결하고, 미들웨어도 추가하면 API 호출 준비 끝
로그인 시 흐름은
1. 로그인 성공
2. 액세스 토큰 받음
3. 액세스 토큰으로 유저 정보 받아옴
4. RTK에 해당 유저 정보 저장
이렇게 흐른다.
const dispatch = useDispatch();
현재 상태를 읽을 필요는 없고, 여기서는 유저 정보를 저장만 해주면 되므로 useDispatch 훅을 선언해 준다.
const [loginUser, { isLoading }] = useLoginMutation(); // isSuccess, isError
const [getUserInfo] = useGetUserInfoMutation();
const onSubmit = async (data: SignInSchema) => {
try {
const result = await loginUser(JSON.stringify(data)).unwrap();
console.log('로그인 성공', result);
setErrorMessage(''); // 에러 초기화
// 자동 로그인 체크 -> 로컬 스토리지
// 자동 로그인 체크 X -> 세션 스토리지
const storage = isChecked ? localStorage : sessionStorage;
// 액세스 토큰 저장
storage.setItem('accessToken', result?.access_token);
const userData = await getUserInfo({});
dispatch(setUser({ user: userData.data }));
} catch (err) {
console.error('로그인 실패:', err);
const errorObj = JSON.parse(JSON.stringify(err));
const status = errorObj?.status; // status에 접근
switch (status) {
case 401:
setErrorMessage('이메일 혹은 비밀번호를 확인해주세요.');
break;
case 500:
setErrorMessage('알 수 없는 오류가 발생하였습니다. 잠시 후 다시 시도해 주세요.');
}
}
};
data에는 이메일과 패스워드가 담긴 객체가 들어있다.
이를 json 형식으로 변환해서 POST 요청을 보내면, 성공 시 데이터가 반환되고 실패 시 에러가 반환된다. (unwrap() 메서드)
그리고 이거는 추후에 수정할 예정인데,
(보안 이슈)
현재는 자동 로그인 체크 여부에 따라 로컬 스토리지에 저장하거나 세션 스토리지에 저장하도록 구현했다.
그리고 에러가 난 경우 코드에 따라 error message를 다르게 하고 싶었는데,
const errorObj = JSON.parse(JSON.stringify(err));
const status = errorObj?.status; // status에 접근
switch (status) {
case 401:
setErrorMessage('이메일 혹은 비밀번호를 확인해주세요.');
break;
case 500:
setErrorMessage('알 수 없는 오류가 발생하였습니다. 잠시 후 다시 시도해 주세요.');
}
err.status로 바로 접근이 불가능했다.
err 객체를 직렬화(JSON.stringify) 후 다시 파싱(JSON.parse)하는 것은,
오류 객체에서 열거 가능한 속성만 일반 객체로 변환해 접근하기 쉽게 만들기 위함이다.
status가 열거 가능한 속성이라서, 직렬화 후 다시 파싱하는 과정이 필요했던 것.
errorObj.status로 접근하니 아주 접근이 잘 됐다.
const userData = await getUserInfo({});
dispatch(setUser({ user: userData.data }));
const baseQuery = fetchBaseQuery({
baseUrl: 'api',
prepareHeaders: headers => {
const token = localStorage.getItem('accessToken') || sessionStorage.getItem('accessToken');
if (token) headers.set('Authorization', `Bearer ${token}`);
return headers;
},
});
현재 baseQuery에 로컬 스토리지나 세션 스토리지에 액세스 토큰이 있는 경우, 그 토큰을 헤더에 담아서 보내도록 되어 있어서,
바로 getUserInfo로 GET 요청을 보내면 유저 데이터를 받아올 수 있다.
이 받은 유저 데이터로
Redux 상태에 사용자 정보를 저장하는 액션(setUser)을 디스패치 하면 Redux에 잘 저장되는 것을 볼 수 있다.
이렇게!
크롬의 확장 프로그램인 Redux dev tool에서 확인이 가능하다.
'개발 > 프로젝트' 카테고리의 다른 글
Next.js에서 RTK Query로 카카오 로그인 구현하기 (1) | 2025.01.09 |
---|---|
redux-persist로 Redux 상태 유지하기 (0) | 2025.01.08 |
RTK Query를 사용하여 회원가입 API 구현하기 (1) | 2025.01.06 |
react-hook-form, zod를 활용하여 회원정보 수정 페이지 제작하기 (1) | 2025.01.04 |
dayjs를 활용하여 생년월일 선택하는 드롭다운 생성하기 (1) | 2024.12.28 |