티스토리 뷰

원래는 search 페이지의 UI를 시작해야 하지만, 공공데이터를 가지고 오기 위해 redux toolkit을 이용한 slice를 미리 만들어두려고 했다가, typescript를 이용한 redux toolkit 사용 방법은 조금 다르다는 것을 깨달았다. 원래 쓰던 slice 코드는 다음과 같다.

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios';

const API_URL = '';

export const 함수이름  = createAsyncThunk('slice이름/', async (payload, { rejectWithValue }) => {
	let result = null;

	try {
		const response = await axios.get(API_URL);
		result = response.data;
	} catch (err) {
		result = rejectWithValue(err.response);
	}

	return result;
});

const slice이름 = createSlice({
	name: 'slice이름',
	initialState: {
		data: null,
		loading: false,
		error: null
	},
	reducers: {},
	extraReducers: {
		[함수이름.pending]: (state, { payload }) => {
			return { ...state, loading: true }
		},
		[함수이름.fulfilled]: (state, { payload }) => {
			return {
				data: payload, 
				loading: false,
				error: null
			}
		},
		[함수이름.rejected]: (state, { payload }) => {
			return {
				...state, 
				loading: false,
				error: {
					code: payload.status ? payload.status : 500,
					message: payload.statusText ? payload.statusText : 'Server Error'
				}
			}
		}
	},
});

export default slice이름.reducer;

비동기처리를 위한 createAsynxThunk를 사용했다.

이 코드를 typescript를 사용하여 이용하려 하니 오류가 많이 떴다. 공식 문서를 읽어보니 기본적인 틀은 비슷한데, 타입 지정 등이 필요한 모양이었다.

 

먼저 store에 추가할 게 있다.

// src/Store.tsx

// 기존 코드
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
	reducer: {
		   
	}
});

export default store;

// 추가 코드
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

이렇게 RootState 타입과 AppDispatch 타입을 정해줘야 한다. 이건 Hook인 useDispatch와 useSelector에 사용할 타입이다.

공식 문서에서는 이 타입을 적용한 커스텀 Hook을 만들기를 권하고 있다. 이유는 다음과 같이 설명하고 있다.

1. useSelector의 경우 매번 (state: RootState)를 입력할 필요가 없다.
2. useDispatch의 경우 기본 유형은 thunk를 알지 못한다. thunk를 올바르게 디스패치 하려면 thunk 미들웨어 유형이 포함된 store의 사용자 지정 AppDispatch 유형을 사용하고, useDispatch에 적용해야 한다. 미리 입력된 useDispatch Hook을 추가하면 필요한 곳에서 AppDispatch를 가지고 오는 것을 잊지 않을 수 있다.

그러니까 즉, useSelector와 useDispatch에 각각 RootState와 AppDispatch 타입을 적용해야 하는데, 항상 이걸 import해서 불러오지 말고 커스텀 Hook으로 만들어 편하게 쓰라는 이야기 같다.

 

공식 문서에서는 이 커스텀 Hook을 이렇게 소개하고 있다.

// src/Hook.ts
import { useDispatch, useSelector } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch } from './store'

export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

 

이제 다시 Slice로 돌아가보자. 최종적으로 고친 코드는 다음과 같다.

// src/Slice/InfoSlice.tsx

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios';

const API_URL = '';

class ErrorClass extends Error {
	response?: {
		data: any;
		status: number;
		headers: string;
	};
}

interface info {
	[key: string]: string | number
}

interface initialState {
	data: info | null,
	loading: boolean,
	error: ErrorClass | null
}

export const getLossList = createAsyncThunk<info, info, {rejectValue: (ErrorClass) }>('LossSlice/getLossList', async (payload, { rejectWithValue }) => {
	let result = null;

	try {
		const response = await axios.get(API_URL);
		result = response.data;
	} catch (err) {
		if(err instanceof ErrorClass) {
			result = rejectWithValue(err);
		}
	}

	return result;
});

const LossSlice = createSlice({
	name: 'LossSlice',
	initialState: {
		data: null,
		loading: false,
		error: null
	} as initialState,
	reducers: {},
	extraReducers: (builder) => {
		builder.addCase(getLossList.pending, (state, {payload}) => {
			state.loading = true;
		}).addCase(getLossList.fulfilled, (state, {payload}) => {
			state.loading = false;
			state.data = payload;
		}).addCase(getLossList.rejected, (state, {payload}) => {
			state.loading = false;
			if(typeof payload !== 'undefined') {
				state.error = payload;
			}
		});
	}
});

export default LossSlice.reducer;

 

여기서 차근차근 살펴보자. 가장 첫 부분은 state와 error, dispatch로 인해 넘겨받을 인수의 클래스 및 인터페이스를 정한 부분이다.

// 에러 타입 지정을 위한 클래스
class ErrorClass extends Error {
	response?: {
		data: any;
		status: number;
		headers: string;
	};
}

// 넘겨받은 인수의 타입 지정을 위한 인터페이스
interface info {
	[key: string]: string | number
}

// state의 타입 지정을 위한 인터페이스
interface initialState {
	data: info | null,
	loading: boolean,
	error: ErrorClass | null
}

 

이 타입들을 지정한 이유는 다음에 나온다. 바로 createAsyncThunk를 쓰는 부분이다.

export const getLossList = createAsyncThunk<info, info, {rejectValue: (ErrorClass) }>('LossSlice/getLossList', async (payload, { rejectWithValue }) => {
	let result = null;

	try {
		const response = await axios.get(API_URL);
		result = response.data;
	} catch (err) {
		if(err instanceof ErrorClass) {
			result = rejectWithValue(err);
		}
	}

	return result;
});

 

주목할 부분은 이 부분이다.

createAsyncThunk<info, info, {rejectValue: (ErrorClass) }>

payloadCreator의 두 번째 인수인 thunkApi는 thunk 미들웨어의 dispatch, getState, 추가 인수와 RejectWithValue 함수를 포함하는 객체다. 그런데 이런 인수를 사용하려고 할 때, 인수의 유형을 유추할 수 없기 때문에 몇 가지 정의를 해줘야 한다. 다른 것도 정의해야 하는 게 있는데, 바로 payloadCreator의 return 값의 타입과, 첫 번째 인수로 들어올 것의 타입이다. 순서는 다음과 같다.

createAsyncThunk<payloadCreator의 리턴값 타입, 첫 번째 인수의 타입, thunkApi의 타입>

여기서 thunkApi의 타입은 다음과 같이 있다.

type AsyncThunkConfig = {
  // thunkApi.getState의 리턴값의 타입
  state?: unknown
  
  // thunkApi.dispatch의 타입
  dispatch?: Dispatch
  
  // thunkApi.extra로 전달될 thunk 미들웨어에 대한 extra 타입
  extra?: unknown
  
  // rejectWithValue의 첫 번째 인수로 전달될 rejectedAction.payload의 타입
  rejectValue?: unknown
  
  // serializeError 옵션 콜백의 리턴값의 타입
  serializedErrorType?: unknown
  
  // getPendingMeta 옵션 콜백의 리턴값이며 pendingAction.meta에 병합될 값의 타입.
  pendingMeta?: unknown
  
  // fulfillWithValue의 두 번째 인수로 전달되어 fulfilledAction.meta에 병합될 값의 타입
  fulfilledMeta?: unknown
  
  // rejectWithValue의 두 번째 인수로 전달되어 rejectedAction.meta에 병합될 값의 타입
  rejectedMeta?: unknown
}

나는 일단 당장 필요해보이는 것만 넣어봤다. 필요하면 추가할 예정이다.

catch문에서 에러 핸들링을 위해 에러 클래스에 포함이 되면 결과에 넣도록 했다.

 

다음은 이 부분이다.

const LossSlice = createSlice({
	name: 'LossSlice',
	initialState: {
		data: null,
		loading: false,
		error: null
	} as initialState,
	reducers: {},
	extraReducers: (builder) => {
		builder.addCase(getLossList.pending, (state, {payload}) => {
			state.loading = true;
		}).addCase(getLossList.fulfilled, (state, {payload}) => {
			state.loading = false;
			state.data = payload;
		}).addCase(getLossList.rejected, (state, {payload}) => {
			state.loading = false;
			if(typeof payload !== 'undefined') {
				state.error = payload;
			}
		});
	}
});

export default LossSlice.reducer;

as를 이용해 initialState에 타입을 결정해줬다.

typescript를 이용해 redux toolkit을 사용할 때는 builder 콜백을 이용하는 것을 권장한다고 공식 문서에 적혀 있길래 바꾸어봤다. export 해서 내보내면 끝이다.

 

여러 블로그를 봤지만 그래서 왜 이렇게 하는 거지? 하는 의문점이 자꾸 생겨서 찾아본 결과... 공식 문서가 가장 큰 해답을 준 것 같다. 번역을 해서 봐야 한다는 불편함이 있지만 가장 해답이 잘 되어있는 것 같다.

 

 

 

 

 

참고한 곳

- Redux Toolkit 공식 문서

- 블로그 1

- 블로그 2

- 블로그 3

- 블로그 4

'개인 프로젝트 > Finding-Item' 카테고리의 다른 글

개인 프로젝트 1.  (0) 2023.02.13
개인 프로젝트 0.  (0) 2023.02.13
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함