
이번 한 주간 프로젝트를 하면서 가장 어려웠던 부분인 "비동기처리 함수를 타이핑하고 Zustand Store에서 관리"하는 방법에 대해 공유해 보고자 한다.
이번 프로젝트에서는 비즈니스 모델에 맞게 적절한 데이터를 적절한 타이밍에 사용자에게 보여주어야 했다. 따라서 원격으로 관리될 DB에 대한 데이터 CRUD를 하는 다양한 비동기처리 함수들이 적절히 구현되어야 했다. 또한 서비스의 다양한 곳에서 그러한 비동기처리 함수들이 호출될 수 있으므로, 그 비동기처리 함수들을 Store에 저장하고 필요한 곳에서 가져와서 사용할 수 있도록 하고자 했다. 또한, 복잡한 비즈니스 로직(혹은 상태관리 로직)을 컴포넌트단으로부터 (일부분) 분리하여, 컴포넌트단에서는 UI/UX로직을 중점적으로 볼 수 있도록 하는 효과도 가질 수 있었다.
그런데 이를 타입스크립트로 타이핑을 하려고 하니 쉽지 않았다. 몇 번의 수정을 거쳐 현재까지 타이핑된 기록을 공유한다.
Store는 다양한 슬라이스 단위로 나뉠 수 있었다. 로그인, 회원가입, 로그아웃 등과 같은 로직을 관리하는 AuthSlice, 상품에 대한 정보 조회 및 수정과 같은 로직을 관리하는 OrderSlice 등등으로 나눠질 수 있다.
먼저 create Store를 해 주었다.
import { create } from 'zustand'
import { createAuthSlice } from './authSlice'
export const useBoundStore = create<AuthSlice>()((...a) => ({
...createAuthSlice(...a),
}))
나는 AuthSlice를 담당했으므로 이를 중심으로 설명하겠다. 먼저 타입을 보자.
interface AuthSlice {
//전역 상태
userToken: string;
userBasicInfo: UserBasicInfoType;
//비즈니스 로직 관련 함수
signUp: (UserInput: UserInputType) => void;
verifyEmail: (email: string) => void;
login: (email: string, password: string) => void; // 인증
}
이 인터페이스를 실제로 구현한 코드를 보면서 설명해 보겠다.
export const createAuthSlice: StateCreator<
AuthSlice,
[]
> = (set) => ({
userBasicInfo: {} as UserBasicInfo,
userToken: "",
verifyEmail: (email: string) => {
requestEmailVerification(email)
},
signUp: (UserInput: UserInputType)=>{
requestSignUp(UserInput)
},
login: async (email: string, password: string) => {
const userLoginResponse = await userLogin(email, password);
// user 토큰 값 저장
set(() => ({
userToken: userLoginResponse.token.accessToken,
userDetailInfo: userLoginResponse,
}));
// user 정보 저장
},
})
verifyEmail과 signUp은 requestEmailVerification과 requestSignUp이라는 함수를 각각 내부적으로 호출한다. requestSignUp을 보자.
const requestSignUp: (arg: UserInputType) => void = async (UserInput: UserInputType) => {
try {
const response = await axios.post<UserInputType, AuthResponseType>(
`${BASE_URL}/users/`,
UserInput,
)
if (response.data.ok === 1) {
alert("회원가입이 완료되었습니다.")
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
catch(Error: any) {
if (Error.response){
alert(Error.response.data.message)
}
console.error("Error:", Error);
}
}
이 메서드는 내부에서async await을 사용하고 있다. 그러나 이 함수는 아무것도 반환하고 있지 않으며, 반환이 필요하지도 않다.
반면에, login은, await을 통해서 비동기처리 함수(userLogin)가 완료되고 반환한 값을 받아서 그 값을 가지고 set을 사용한다. 반환값 또한 Promise<void>로 타이핑되었다. 만약 이런 로직들을 login 메서드에서 직접 구현하지 않고 verifyEmail과 signUp처럼 내부적으로 호출하려면 매개변수로 set을 넘겨주어야 하는데, set의 타입이 매우 복잡했기 때문에 매개변수 타이핑을 하여 넘겨주기가 어려웠다. 따라서 async를 메서드에 직접 붙여주면서 사용하게 되었다.
