Poke Docs 만들기 - NextJS, react-query
서론에서 썻던것과 같이 페이지는 총 3개로 구성하였습니다.
- index.tsx
- docs.tsx
- item/[id].tsx
일단 Next js 에 나온것 과 같이 시작을 하였습니다.
Next app 생성
npx create-next-app pocket-docs
npx create-next-app pocket-docs
기본적으로 typescript, eslint, prettier 를 추가하였습니다.
- nextJS 에서 typescript 도입은 생각외로 굉장히 간단한데요.
Next js Basic Features TypeScript <- 문서에 나와있는데로 하면 쉽게 관련 파일과 typescript 를 설치할 수 있습니다.
- eslint 는 기본적으로 airbnb 기반으로 작성하였고 prettier 는 다음과 같이 추가하였습니다.
.prettierrc
{
"singleQuote": true, // ' 따옴표 사용
"semi": true, // 세미콜론 여부
"useTabs": false, // tab 사용 여부
"tabWidth": 2, // tab 너비
"trailingComma": "all", // 요소에 , 추가
"printWidth": 80, // max 가로 length
"arrowParens": "avoid" // 화살표 함수 괄호 사용 방식
}
- emotion을 사용하게 되면 emotion이 자동으로 className을 난독화 해버리는데 이는 디버깅에서 참 쉽지 않습니다.
예를 들면 이런식.... (코드상에서 어떤 클래스인지 찾을 수가 없음)
따라서 .babel을 이용하여 특정 className을 정의하여 디버깅을 쉽게 할 수 있게 해줄 수 있습니다.
yarn add babel-plugin-emotion --dev
.babelrc
{
"presets": ["next/babel"],
"plugins": [
[
"@emotion",
{
"autoLabel": "dev-only",
"labelFormat": "[dirname]-[filename]-[local]"
}
]
]
}
그럼 이제 css-암호화-dirname-filename-className 으로 되어서 디버깅하기가 아주 쉬워집니다.
- 마지막으로 API fetch 를 위해 redux 를 사용하지 않고 react-query 를 사용하였는데, redux 에 수많은 보일러플레이트 코드들도 보기 싫고 간단한 프로젝트인 만큼 가볍게 가고 싶어서 선택하게 되었습니다.
또한 graph QL 에 대한 관심도도 한 몫 한것 같습니다.
공식문서가 아주 잘 되어있고 서론에서 언급했던 SSR 도 아주 잘 지원됩니다. 특히 Next JS 부분이 있다는것도 상당히 매력적이죠.
이렇게 대략적인 프로젝트 구조 설명은 끝난것 같습니다. 뭐 그외에 여러가지 모듈들이 설치가 되어있는데요.
- axios <- 누구나 아는 모듈
- object-key-converter <- snake_case <-> camelCase 로 변환해주는 모듈 (제가 만들었어요. PR 환영!)
- eslint-config-prettier, eslint-plugin-prettier <- eslint/prettier 충돌 방지를 위한 모듈
- eslint- <- 그 외에 eslint-aribnb 사용시 자동으로 추가되는 모듈
- emotion 관련 모듈
{
"name": "pocket-docs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint ./ --ext .js,.jsx,.ts,.tsx --fix"
},
"dependencies": {
"@emotion/core": "^11.0.0",
"@emotion/react": "^11.1.4",
"@emotion/styled": "^11.0.0",
"axios": "^0.21.1",
"next": "10.0.6",
"object-key-converter": "^1.0.4",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-query": "^3.7.1"
},
"devDependencies": {
"@types/node": "^14.14.25",
"@types/react": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"babel-plugin-emotion": "^11.0.0",
"eslint": "^7.19.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^2.2.1",
"typescript": "^4.1.3"
}
}
뭐 딱히 특히한건 없어보이네요. 기본적인 프로젝트 세팅이고 추가로 제가 진행하려고 하는 simple create react app 프로젝트인데 거기에는 정말 필요한 모듈만 설치하고 react app 을 만들기 위한 프로젝트인데 진전이.... ㅋㅋㅋ(PR 환영)
Next 구조
index.tsx
먼저 pages/index.tsx 를 구성하였습니다. 메인화면으로 크게 하는일은 없고 snowflake 효과를 포켓몬으로 줘보기로 하였습니다.
그러면 일단 포켓몬 리스트를 가져와야하는데
- GET
https://pokeapi.co/api/v2/pokemon?limit={number}&offset={number}
위에 api 를 호출하면 포켓몬 리스트가 가져와지는것을 볼 수 있습니다.
Next에 유용한점을 사용하기 위해 prefetch 를 이용하여 api 를 불러보도록 하였습니다.
기존에 제가 Next 를 사용했을 때에는 getInitialProps 뿐이였는데 버전업이 되면서 여러가지가 바뀌었더군요.
- getStaticProps: static data를 위한 data fetching
- 만약 dynamic route item/[number].tsx 를 사용하는 경우 getStaticPaths 을 사용하여 명시적으로 정의된 path를 설정해줘야함 - getServerSideProps: SSR를 위한 data fetching
따라서 일반적인 pages/index.tsx 에서는 getStaticProps 을 사용하였고 동적인 pages/item/[number].tsx 에서는 getStaticProps, getStaticPaths 를 같이 사용하였습니다.
pages/index.tsx
import { getPocketmonList } from 'apis/getPokemonList';
import { Layout } from 'components/Layout';
import { Thumbnailes } from 'components/Thumbnailes';
import { GetStaticProps } from 'next';
import React from 'react';
import { QueryClient } from 'react-query';
import { dehydrate } from 'react-query/hydration';
const Home = () => (
<Layout>
<Thumbnailes />
</Layout>
);
export const getStaticProps: GetStaticProps = async () => {
const queryClient = new QueryClient();
await queryClient.prefetchQuery('pokemonlist', getPocketmonList, {
staleTime: 10000,
});
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};
export default Home;
Thumbnailes.tsx
components/Thumbnailes.tsx
import { PokemonThumbnail, Thumbnail } from 'components/Thumbnail';
import { IMAGE_URL, TITLE_IMAGE } from 'constants/common';
import { useGetPokemonList } from 'hooks/useGetPokemonList';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import * as Styles from './styles';
export const Thumbnailes = () => {
const router = useRouter();
const { data, isLoading } = useGetPokemonList();
const pokemonList = useMemo(() => {
if (data) {
return data.results.map((item, index) => ({
...item,
number: index + 1,
}));
}
return [];
}, [data]);
const [thumbnailes, setThumbnailes] = useState<PokemonThumbnail[]>([]);
useEffect(() => {
setThumbnailes(
pokemonList.map(pokemon => ({
...pokemon,
image: `${IMAGE_URL}${pokemon.number}.png`,
})),
);
}, [pokemonList]);
const handleClickFindPokemon = useCallback(() => {
router.push('docs');
}, [router]);
const pokemonImages = useMemo(
() => (
<>
{thumbnailes.map((thumbnail, index) => (
<Styles.Thumbnail
delay={Math.random() * 5}
left={Math.random() * 100}
// eslint-disable-next-line react/no-array-index-key
key={index}
>
<Thumbnail pokemon={thumbnail} />
</Styles.Thumbnail>
))}
</>
),
[thumbnailes],
);
if (isLoading || !data) {
return <div>Loading...</div>;
}
return (
<Styles.Container>
<Styles.Image src={TITLE_IMAGE} alt="titleImage" />
<Styles.Text onClick={handleClickFindPokemon}>
{'포켓몬 찾기 >'}
</Styles.Text>
{pokemonImages}
</Styles.Container>
);
};
useGetPokemonList.ts
hooks/useGetPokemonList.ts
import { getPocketmonList } from 'apis/getPokemonList';
import { useQuery } from 'react-query';
export const useGetPokemonList = () =>
useQuery('pokemonlist', getPocketmonList);
이런식으로 getStaticProps 내부에서 QueryClient 를 호출하고 prefetchQuery를 해주면 react-query 가 prefetchQuery key(pokemonlist) 에 따라 query 를 prefetch 하고, 컴포넌트 단에서 useQuery 를 사용하면 그 key 에 따라 prefetch 된 값을 빠르게 가져올 수 있게 됩니다.
react-query에서 제공해주는 Using Hydration 방식으로 구현하였습니다.
이렇게 되면 getStaticProps 에서 pokemonlist key에 대한 prefetch 를 진행하게 되고, 클라이언트에서는 useQuery를 호출하였을때에 기존에 prefetch 하였던 data 를 리턴하게 됩니다.
_app.tsx
_app.tsx
const queryClient = new QueryClient();
queryClient.setDefaultOptions({
queries: {
staleTime: Infinity,
},
});
function MyApp({ Component, pageProps }: MyAppProps) {
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} />
</Hydrate>
</QueryClientProvider>
);
}
export default MyApp;
queryClient 에 staleTime 을 Infinity 로 해주어야 서버 사이드(getStaticProps)에서 prefetch 한 query가 클라이어트 사이드(useQuery) 에서 호출 하였을때 값이 유지되는것을 볼 수 있습니다.
만약 다음과 같이 클라이어트 사이드(useQuery) 에서 staleTime 을 정해주면 그 시간마다 다시 query 를 요청하는것을 볼 수 있습니다.
useQuery('key', api, { staleTime: 1000})
위와 같이 react-query 를 Next 에 적용시켜 사용하는 법을 알아보았는데요. 아직 많은 예제들이 없고 특히 한국어로 되어있는 예제들은 거의 없어서 잘 적용되었는지는 모르겠습니다. 하하
결론
- _app.tsx 에서 queryClient 설정, Hydrate components 로 감싸기
- pages/[name].tsx 에서 prefetch 시 stale time 적용. 만약 stale time 적용하지 않을 시에 설정이 Infinity가 되어서 새로운 쿼리로 업뎃이 안됌
- 클라이언트 단에서는 useQuery를 통해서 값을 가져오는데 만약 prefetch 가 된 값이 있다면 그 값을 가져오게됨. 역시 useQuery 에 stale time 을 적용하게 되면 그 시간이 지난 후 새롭게 query 를 요청하게 됨.
다음에는 index page 에 snowflake 효과에 대하여 포스팅해보도록 하겠습니다.
Author And Source
이 문제에 관하여(Poke Docs 만들기 - NextJS, react-query), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@eomttt/Poke-Docs-만들기-NextJS-react-query저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)