채용을 위해 개발자 면접을 보면 React-Query이 좋다는 것은 알지만 왜 좋은지를 모르고 있으며, 다른 대안인 SWR은 언급하는 경우가 없었다.
여러 기업에서(예를 들면 토스라든지) React-Query를 사용해서 그런지 알려졌으나 SWR은 잘 알려지지 않아 아쉽다.
현재 회사에서 SWR를 사용하는 기간이 늘어감에 따라 어떻게 사용하는 게 좋은지 의견을 나누는 시간을 여러 번 가졌다.
개발자끼리 서로가 만드는 방법을 맞추지 못하고 어떻게 사용하는 것이 좋은지 알지 못하는 상태에서 진행하는 것은 최악으로 가는 길이라는 생각에 리팩토링한다는 생각을 가지고 SWR을 더 잘 사용할 수 있도록 몇 가지를 개선해보았다.
회의를 진행하고 나서야 더 좋은 방안이 나오게 되었고, 오늘은 개선된 내용을 작성해보았다.
SWR이란 | Redux 말고 SWR |
SWR 더 나아가기 | SWR / react-query useMutation처럼 사용하기 |
개선사항
- SSR(Server-Side-Rendering) & Pre-rendering에서 SWR 같이 사용하기
- Nextjs Pre-rendering 사용하기
- useMutation + key는 어떻게 사용하는게 좋을까?
- useMutation으로 isLoading 처리하기
SWR을 제대로 사용한 지 1년이 지난 시점에서 이제서야 위의 내용을 작성했지만, 실제로는 빠르게 개선되어 정착되었다. 더구나 같이 개발하는 프론트개발자와 호흡이 좋아서 빨리 끝났다고 생각한다.
현재 개발하는 자리톡의 사이즈가 커지면서 개선되었다고 생각되는 주제로 작성하여, 다른 곳에서는 맞지 않을 수 있습니다.
SSR & Pre-rendering에서 SWR 같이 사용하기
커뮤니티 상세화면을 만든다고 생각해보자.
먼저 우리는 SWR을 사용하게 되면 Post라는 컴포넌트 파일에서 SWR을 작성할 것이다.
const { data } = useSWR<ApiReturn<ICommunityPost>>(key, fetcher, option);
Post.tsx
useSWR
을 사용하게 되면 필수로 key와 fetcher를 넣어주며, 결과로 data를 가져오게 된다.
이 코드를 다른 곳에서 사용할 수 있다고 하면 아래와 같이 Custom Hook으로 만들어 사용할 것이다.
export const useGetPost = (postPk: number) => {
const { data } = useSWR<ApiReturn<ICommunityPost>>(
key,
fetcher,
option
);
return {
data: data?.data ?? InitCommunityPostDetail
};
};
getPost.tsx
useGetPost
라는 Custom Hook은 여러 컴포넌트에서 postPk
만 넘겨주면 사용할 수 있게 되었다.
이렇게 Client-Side에서만 API를 호출한다면 useSWR
를 사용하는 것만으로 끝났다.
이후 SEO 작업하게 되면서 Nextjs에 getServerSideProps
를 사용하여 데이터를 가져와서 Meta Tag에 넣어주었다.
// highlight-start
export async function getServerSideProps (ctx) {
const postPk = ctx.query['postPk']
// `getServerSideProps`는 서버 사이드에서 실행한다.
const post = await getPost(postPk)
return {
props: {
post: post
}
}
}
// highlight-end
export default function Page({ post }) {
return (
<>
<Head>
<title>{`${post?.title ?? ''}`}</title>
<meta name='description' content={`${post?.content ?? ''}`} />
</Head>
<Post />
</>
)
}
post/[postPk].tsx
간단하게 표현하면 위와 같이 getServerSidePorps
에서 getPost
로 요청하여 post 데이터를 가져와 Page의 Props로 넘겨 ServerSide에서 post 데이터가 포함된 페이지를 만들어 준다.
여기서 const post = await getPost()
를 다시 생각해보면 useGetPost
Hook의 fetcher와 동일하다는 것을 알 수 있다.
같이 사용할 수 있도록 개선하면 아래와 같다.
// highlight-start
export const fetchGetPost= (postPk: number) => {
return fetcher<ICommunityPost>(`community/post/${postPk}`)
};
// highlight-end
export const useGetPost= (postPk: number) => {
const { data } = useSWR<ApiReturn<ICommunityPost>>(
key,
(() => fetchGetPost(postPk)), // highlight-line
option
);
return {
data: data?.data ?? InitCommunityPostDetail
};
};
getPost.tsx
export async function getServerSideProps (ctx) {
const postPk = ctx.query['postPk']
const post = await fetchGetPost(postPk) // highlight-line
return {
props: {
post: post
}
}
}
export default function Page({ post }) {
return (
<>
<Head>
<title>{`${post?.title ?? ''}`}</title>
<meta name='description' content={`${post?.content ?? ''}`} />
</Head>
<Post />
</>
)
}
post/[postPk].tsx
fetchGetPost
함수를 만들어 useGetPost
와 getServerSideProps
에서 사용했다. 이제 Client-Side와 Server-Side에서 사용할 수 있도록 개선하였다.
useMutation key는 어떻게 사용하는 것이 좋은가?
우리는 이미 Server-Side에서 데이터를 가져오는 행위를 했다. 그렇다면 Client-Side에서 동일한 API를 호출할 필요가 있는지 생각해볼 수 있다.
Server-Side에서 가져온 데이터를 SWR에게 어떤 API의 데이터인지 알려줄 수 있다면 API 호출을 줄일 수 있을 것이다.
SWR 공식 홈페이지에서 알려주는 방식은 아래와 같다.
export async function getServerSideProps (ctx) {
const postPk = ctx.query['postPk']
const post = await fetchGetPost(postPk)
return {
props: {
// highlight-start
fallback: {
'/community/post': post
}
// highlight-end
}
}
}
function Post() {
// `data`는 `fallback`에 있기 때문에 항상 사용할 수 있다.
const { data } = useSWR('/community/post', fetcher)
return <h1>{data.title}</h1>
}
export default function Page({ fallback }) {
// `SWRConfig` 경계 내부에 있는 SWR hooks는 해당 값들을 사용한다.
return (
<>
// highlight-start
<SWRConfig value={{ fallback }}>
<Post />
</SWRConfig>
// highlight-end
</>
)
}
살펴보면 getServerSideProps
에서 가져온 데이터를 fallback에 key, value로 담아서 내려주며, 넘겨준 값을 SWRConfig의 value로 넣어주며 이후 useSWR
의 key에 fallback에 입력한 key의 값과 같다면, Server-Side에서 내려받은 데이터를 사용한다.
코드를 개선해보자.
// highlight-start
export const generateGetPostKey= (postPk: number): string => {
return `/community/posts/${postPk}`;
};
// highlight-end
export const fetchGetPost= (postPk: number) => {
return fetcher<ICommunityPost>(generateGetPostKey(postPk))
};
export const useGetPost= (postPk: number) => {
const { data } = useSWR<ApiReturn<ICommunityPost>>(
generateGetPostKey(postPk), // highlight-line
(() => fetchGetPost(postPk)),
option
);
return {
data: data?.data ?? InitCommunityPostDetail
};
};
getPost.tsx
export async function getServerSideProps (ctx) {
const postPk = ctx.query['postPk']
const key = generateGetPostKey(postPk) // highlight-line
const post = await fetchGetPost(postPk)
return {
props: {
post: post,
fallback: {
[key]: post
}
}
}
}
function Post() {
const router = useRouter()
const postPk = router.query['postPk']
const { data } = useGetPost(postPk)
return <h1>{data.title}</h1>
}
export default function Page({ post }) {
return (
<>
<Head>
<title>{`${post?.title ?? ''}`}</title>
<meta name='description' content={`${post?.content ?? ''}`} />
</Head>
<SWRConfig value={{ fallback }}>
<Post />
</SWRConfig>
</>
)
}
post/[postPk].tsx
generateGetPostKey
함수를 사용해 key를 만들도록 구성했다. SWR를 사용하면서 좋은 key는 API URL이라고 생각하게 되었다. Unique하면서 개발자가 많은 생각을 하지 않아도 되었다. 함수를 만들어, fallback, useSWR, fetcher 세 군데에 사용하였다.
이제 Server-Side에서 요청한 데이터를 Client-Side까지 가져오게 되면서 초기에 SWR로 데이터를 가져오는 호출을 줄였다.
useMutation으로 isLoading 처리하기
이전글에서 SWR에서 지원하지 않지만 React-Query처럼 useMutation을 사용하도록 도와주는 NPM Library를 사용했다.
회사 내에서 사용하다 보면 아쉬운 부분이 남기 마련이다. 제일 아쉬웠던 부분은 useMutation을 실행하고 API 통신이 이루어지는 동안 Loading 화면 또는 버튼을 disable 처리하여 사용자가 연속으로 API 호출할 수 없도록 하는 것이 힘들다는 것이었다.
use-mutation library 에는 isLoading 기능이 없어서, Library 코드를 Clone하여 내부 라이브러리로 만들어서 기능을 추가했다.
const [{ status, data, error }, unsafeDispatch] = useReducer<
Reducer<State, Action>
>(
function reducer(_, action) {
if (action.type === 'RESET') {
return { status: 'idle' };
}
if (action.type === 'MUTATE') {
return { status: 'running' };
}
if (action.type === 'SUCCESS') {
return { status: 'success', data: action.data };
}
if (action.type === 'FAILURE') {
return { status: 'failure', error: action.error };
}
throw Error('Invalid action');
},
{ status: 'idle' }
);
기존 useMutation.ts
기존 코드를 분석하면, action.type
에 따라서 status, data, error 값이 변경되는 것을 알 수 있다.
즉 API 호출 상태에 따라서 status가 변경되지만 우리는 더 직관적으로 사용하기 위해서 isLoading이라는 값을 추가하였다.
코드에서는 action.type
의 종류는 RESET, MUTATE, SUCCESS, FAILURE가 있다. 여기서 우리가 알아야 하는 것은 MUTATE 상태일 때 isLoading을 true라고 알려주는 것이며, 나머지 상태에서는 false로 반환하면 된다.
수정하면 아래와 같은 형태가 된다.
const [{ status, data, error, isLoading }, unsafeDispatch] = useReducer<
Reducer<State, Action>
>(
function reducer(_, action) {
if (action.type === 'RESET') {
return { status: 'idle', isLoading: false };
}
if (action.type === 'MUTATE') {
return { status: 'running', isLoading: true };
}
if (action.type === 'SUCCESS') {
return { status: 'success', data: action.data, isLoading: false };
}
if (action.type === 'FAILURE') {
return { status: 'failure', error: action.error, isLoading: false };
}
throw Error('Invalid action');
},
{ status: 'idle', isLoading: false }
);
변경된 useMutation.ts
이제 컴포넌트 내부에서 사용할 수 있다. isLoading을 Hook마다 이름을 다르게 하고 싶다면 아래와 같이 작성하면 된다.
const [requestPostAPI, { isLoading: requestPostLoading }] = useCreatePost()
이제 우리는 requestPostLoading
만 확인하면 현재 post가 만들어지고 있는지 알 수 있으며, 버튼을 disable하거나 전체 로딩 페이지를 띄워서 UX적으로 개선할 수 있다.
실제로 어떻게 사용하고 있는지 살펴보기
현재 자리톡에서는 Nx Framework를 사용하고 있어 API 호출하는 모든 useSWR + useMutation을 API 라이브러리 안에서 관리하고 있다.
백엔드에서 사용하는 Swagger 구조를 따라하여 폴더와 파일을 구성하였다. 관리에서 유용하고 많은 고민을 해소할 수 있다는 것을 몸소 느꼈다.
다만 파일 이름은 HTTP Method를 최대한 Prefix로 붙여서 직관적으로 하였고, POST의 경우는 여러 의미로 쓰일 수 있다고 생각하여 register, create 등 직관적인 단어를 Prefix로 붙이고 있다.
모든 파일은 위에서 개선한 형태로 들어가고 있으며, 지금도 문제없이 사용하고 있다.
👋 Outtro
SWR는 React-Query에 비해서 날 것에 가깝다고 생각한다. 그러나 잘 사용한다면 내가 Custom하기 좋다고 생각한다. 현재 글도 작성할 수 있었던 것은 개발자들끼리 이야기하며 불편함을 나누고 어떻게 하면 개선할 수 있을까
를 같이 생각하고 개선하려는 의지가 있어서 가능했다고 생각한다.
앞으로는 React-Query와 비교하여 개선해보고 글을 작성해보려고 한다.
SWR이란 | Redux 말고 SWR |
SWR 더 나아가기 | SWR / react-query useMutation처럼 사용하기 |
SWR / 어떻게 사용하시나요? | SWR / 어떻게 사용하시나요? |