개발순서는 이게 먼저인데 작성하다보니 인증 구현을 먼저 작성했다 🙄
저번주에 Setup한 개발환경에서 본격적으로 구조 설계에 들어갔다.
이번에도 사심을 담아 사내에서 미리 사용할 구조를 설계한다는 기분으로 구현했다. 😏
사내에 이미 nextjs로 구현중인 프로젝트가 있는데 해당 코드를 많이 참고했다.
에러 처리
클라이언트 에러처리는 크게 세 부분으로 구분하였다.
1. 모든 페이지에 공통적으로 처리할 에러
2. 페이지 혹은 관심사별로 처리할 에러
3. 단일 컴포넌트 내 이벤트 핸들러 등에서 처리할 에러
들어가기 전에
httpRequest에서 발생하는 에러는 Axios Interceptor에서 처리하였다.
에러 클래스를 정의하고 http응답에서 내려주는 에러에 따라 다른 에러 인스턴스를 throw한다.
export class ApiError extends Error { | |
code?: ResponseCode | |
redirect?: (url: string) => string | |
} | |
export class CommonApiError extends ApiError { | |
code = RESPONSE.ERROR | |
message = '[SYSTEM] Api Error' | |
} | |
/** | |
* @example 리다이렉트되어야 하는 URL이 있는 에러 클래스 | |
*/ | |
export class RedirectArror extends ApiError { | |
code = RESPONSE.REDIRECT | |
message = '[SYSTEM] Api Error' | |
redirect = (url?: string) => url || ROUTES.ROOT | |
} | |
export class AccessTokenError extends ApiError { | |
code = RESPONSE.INVALID_ACCESS_TOKEN | |
message = '[SYSTEM] Access Token is Expired or Invalid' | |
redirect = (url?: string) => url || ROUTES.ROOT | |
} | |
export const isInstanceOfApiError = (e: unknown): e is ApiError => | |
e instanceof ApiError | |
export const isInstanceOfCommonApiError = (e: unknown): e is CommonApiError => | |
e instanceof CommonApiError | |
export const isInstanceOfRedirectArror = (e: unknown): e is RedirectArror => | |
e instanceof RedirectArror | |
export const isInstanceOfAccessTokenError = ( | |
e: unknown, | |
): e is AccessTokenError => e instanceof AccessTokenError |
export function AuthInterceptor<T>( | |
res: AxiosResponse<Response<T>>, | |
): AxiosResponse { | |
const code = res.data.code | |
switch (code) { | |
case RESPONSE.INVALID_ACCESS_TOKEN: | |
throw new AccessTokenError() | |
case RESPONSE.ERROR: | |
throw new CommonApiError() | |
case RESPONSE.REDIRECT: | |
throw new RedirectArror() | |
default: | |
return res | |
} | |
} |
1. 모든 페이지에 공통적으로 처리할 에러
- API 에러
- Access Token 검증 에러
위 두 에러는 모든 컴포넌트에서 공통적으로 처리하기 위해 __app.tsx를 Error Boundary처럼 사용하기로 하였다.
type AppProps = AppInitialProps | |
interface State { | |
error: Error | null | |
} | |
class Page extends App<AppProps> { | |
static async getInitialProps({ | |
ctx, | |
Component: {getInitialProps: getComponentIntialProps}, | |
}: AppContext): Promise<AppProps> { | |
try { | |
/* 쿠키 검증 중략 */ | |
const props = await (getComponentIntialProps | |
? getComponentIntialProps(ctx) | |
: Promise.resolve({})) | |
const pageProps = { | |
...props, | |
token: letterLogin, | |
} | |
return { | |
pageProps, | |
} | |
} catch (error) { | |
if (isInstanceOfApiError(error) && ctx.req && ctx.res) { | |
apiServerErrorHandler(error, {req: ctx.req, res: ctx.res}) | |
} | |
return { | |
pageProps: {error}, | |
} | |
} | |
} | |
state: State = { | |
error: null, | |
} | |
static getDerivedStateFromError(error: Error) { | |
return {error} | |
} | |
componentDidCatch(error: Error, __: ErrorInfo) { | |
/** add common error */ | |
if ( | |
isInstanceOfCommonApiError(error) || | |
isInstanceOfRedirectArror(error) || | |
isInstanceOfAccessTokenError(error) | |
) { | |
return apiErrorHandler(error) | |
} | |
} | |
render() { | |
const {Component, pageProps} = this.props | |
const {error} = this.state | |
if (error) { | |
return <ErrorPage statusCode={500} /> | |
} | |
return ( | |
<> | |
{/* */} | |
</> | |
) | |
} | |
} | |
export default Page |
2. 페이지 혹은 관심사별로 처리할 에러
각 페이지마다 혹은 관심사별로 묶인 컴포넌트들에 한해서 처리해야 할 에러는 기본 Error Boundary를 사용한다.
1번에서 처리하는 에러는 동일한 처리를 위해 componentDidCatch문을 마찬가지로 추가하였다.
export interface FallbackProps { | |
error: Error | |
} | |
interface Props { | |
fallback: (args: FallbackProps) => ReactNode | |
children: ReactNode | |
withChildren?: boolean | |
} | |
interface State { | |
error: Error | null | |
} | |
export default class ErrorBoundary extends Component<Props, State> { | |
constructor(props: Props) { | |
super(props) | |
this.state = {error: null} | |
} | |
static getDerivedStateFromError(error: Error) { | |
return {error} | |
} | |
componentDidCatch(error: Error, __: ErrorInfo) { | |
if ( | |
isInstanceOfCommonApiError(error) || | |
isInstanceOfRedirectArror(error) || | |
isInstanceOfAccessTokenError(error) | |
) { | |
return apiErrorHandler(error) | |
} | |
} | |
render() { | |
const {error} = this.state | |
const {children, fallback, withChildren = false} = this.props | |
return error ? ( | |
<> | |
{fallback({error})} | |
{withChildren && children} | |
</> | |
) : ( | |
children | |
) | |
} | |
} |
3. 단일 컴포넌트 내 이벤트 핸들러 등에서 처리할 에러
단일 컴포넌트에서 사용될 에러는 단순한 try catch 문으로 작성한다.

SWR+Axios
Data Fetching 라이브러리 SWR + Async HttpRequest 라이브러리 Axios의 조합을 사용하였다.
SWR 공식 홈페이지에서 자세한 가이드와 예시를 볼 수 있다. (한국어 버전도 있어서 쉽게 이해되었다 👍)
SWR GIthub에서 제공하는 예제 중 하나인 axios + typescript 예시를 따르면서 우리 프로젝트에 맞게 변형하였다.
withAxios.ts
Axios 인스턴스 설정이다.
먼저 기본 응답 구조를 code와 result를 갖는 객체로 구현하였다.
{
code : 정상, 에러 응답 코드
message : 에러 메세지
result : 실제 사용할 데이터
}
code값에 따라 다른 에러 인스턴스를 throw하는 Interceptor를 생성 후 사용자 정의 Axios 인스턴스에 인자로 주입한다.
function AuthInterceptor<T>(res: AxiosResponse<Response<T>>): T { | |
const code = res.data.code | |
switch (code) { | |
case RESPONSE.INVALID_ACCESS_TOKEN: | |
throw new AccessTokenError() | |
case RESPONSE.ERROR: | |
throw new CommonApiError() | |
case RESPONSE.REDIRECT: | |
throw new RedirectArror() | |
default: | |
return res.data.result | |
} | |
} | |
export const withAxios = async <T>(request: RequestConfig): Promise<T> => { | |
const instance = axios.create() | |
instance.interceptors.response.use(AuthInterceptor) | |
const response = await instance.request<T, T>({ | |
...request!, | |
baseURL: `${isSSR ? HOST_URL : ''}/api`, | |
}) | |
return response | |
} |
useRequest.ts
useSWR을 사용하는 hook으로 fetcher를 위에서 정의한 withAxios로 두었다. SWR의 key는 AxiosRequest 객체를 stringify한 문자열로 두었다.
interface SwrReturn<Data, Error> | |
extends Pick< | |
SWRResponse<Data, AxiosError<Error>>, | |
'isValidating' | 'revalidate' | 'error' | 'mutate' | |
> { | |
data: Data | undefined | |
response: Data | undefined | |
} | |
export interface SwrConfig<Data = unknown, Error = unknown> | |
extends Omit<SWRConfiguration<Data, AxiosError<Error>>, 'initialData'> { | |
initialData?: Data | |
} | |
export default function useRequest<Data = unknown, Error = unknown>( | |
request: RequestConfig, | |
{initialData, ...config}: SwrConfig<Data, Error> = {}, | |
): SwrReturn<Data, Error> { | |
const { | |
data: response, | |
error, | |
isValidating, | |
revalidate, | |
mutate, | |
} = useSWR( | |
request && JSON.stringify(request), | |
() => withAxios<Data>(request), | |
{ | |
...config, | |
initialData, | |
}, | |
) | |
return { | |
data: response, | |
response, | |
error, | |
isValidating, | |
revalidate, | |
mutate, | |
} | |
} |
실제 사용
useRequest의 사용 예시이다.
interface ProfileContextState { | |
profile: Partial<Profile> | undefined | |
reset: () => void | |
error: Error | undefined | |
} | |
const ProfileContext = createContext<ProfileContextState>( | |
{} as ProfileContextState, | |
) | |
export const ProfileProvider = ({ | |
children, | |
token, | |
}: { | |
children: ReactNode | |
}) => { | |
const { | |
data: profile, | |
error, | |
mutate: refreshProfile, | |
} = useRequest<Profile>( | |
{ | |
url: '/profile', | |
}, | |
{ | |
revalidateOnMount: true, | |
}, | |
) | |
const value = useMemo( | |
() => ({ | |
profile, | |
reset() { | |
refreshProfile() | |
}, | |
error, | |
}), | |
[profile, refreshProfile, error], | |
) | |
return ( | |
<ProfileContext.Provider value={value}> | |
{children} | |
</ProfileContext.Provider> | |
) | |
} | |
export const useProfileContext = () => { | |
const context = useContext(ProfileContext) | |
if (!context) { | |
throw new Error(`You need to wrap UserProvider.`) | |
} | |
return context | |
} |
참고한 자료
'Projects > 넥스터즈 19기' 카테고리의 다른 글
[NCP] Helm 사용하여 Kubernetes 기반으로 서비스 배포하기 (0) | 2021.08.24 |
---|---|
8/20 Nexters 8주차 : Docker 기반 배포 (0) | 2021.08.20 |
7/31 넥스터즈 5주차 : 단위테스트, 디자인 시스템 (0) | 2021.08.01 |
7/16 넥스터즈 3주차 (1) : 인증 구현 (0) | 2021.07.16 |
7/10 2주차 세션 : 해커톤 (0) | 2021.07.11 |