getStaticPaths → getStaticProps 데이터 전달
8/25/2022
작성자 : 홍원배
블로그 상세 포스트(page/[id].tsx)에서 문제가 생겼다.
새로고침하면,
이전게시물, 다음게시물을 나타내는 PostNav 컴포넌트가 사라지는 것이다
Why?
// page/index.ts const Home = ({ tags, posts }: InitialPage) => { const isMainDoor = useAppSelector((state) => state.isMainDoor); const dispatch = useAppDispatch(); useEffect(() => { const postsIDs = posts.map(({ postId }) => postId).reverse(); dispatch(setPostsIDs(postsIDs)); }, [dispatch, posts]); return ( ... ) } // components/PostNav.tsx (page/[id].tsx의 자식컴포넌트) export default function PostNav({ post }) { const dispatch = useAppDispatch(); const postsIDs = useAppSelector((state) => state.postsIDs); const router = useRouter(); return ( <div className={styles.navigation}> {postsIDs[post.order! - 2] && <Link href={`/posts/${postsIDs[post.order! - 2]}`}>이전 게시물</Link>} {postsIDs[post.order!] && <Link href={`/posts/${postsIDs[post.order!]}`}>다음 게시물</Link>} </div> )
index는 posts라는 전체 포스트 데이터에서 가공하여 postIDs라는 ID 정보를 redux store에 저장한다. 그 후, 각 상세 페이지([id].tsx)로 router 이동이 된다면 페이지 내의 PostNav 컴포넌트 안에서 postIDs를 redux로 받아와 .navigation에서 이용한다
문제는 index 페이지를 거치지 않고 상세 페이지(page/[id].tsx) 안에서 새로고침을 눌러 재렌더링 한다면, postIDs를 store에 저장하는 과정이 생략되고, postNav가 받아오는 postsIDs값은 텅빈 배열이 된다는 것이다.
새로고침을 눌러도 nav 정보가 유지되기 위해서는 상세 페이지([id].tsx) 내에서 postsIDs를 처리할 필요가 있다.
그렇다면, 상세페이지를 빌드할 때, 즉 getStaticPaths에서 postsIDs 정보를 만들어서 페이지를 만들어주면 된다.
문제는 paths에서 전달되는 postsIDs의 전달 값이 getStaticProps로 이어지지 않는다는 것이다
console.log를 통해 getStaticPaths에서 보내진 데이터를 찍어보면 postsIDs 라는 프로퍼티는 사라져있다.
물론, getStaticProps에서 다시 API요청을 보내도 되지만, 복잡한 외부 API를 또 호출하고 가공한다는 것은 통신 대역폭을 사용하여 비효율이 커진다.
현재 오피셜 아닌 오피셜로 추천 되는 방법은 cache를 이용하는 것이다. 쉽게 말해, getStaticPath에서 내부 api를 사용해서 fs.writeFile을 사용해 데이터를 저장하여 cache하고, getStaticProps에서 fs.readFile을 통해 불러오는 것이다.
구현해보자
import { getDetailPost, getPostsPath, postNav } from '@/lib'; export const getStaticPaths = async () => { const notionDatabaseID = process.env.NOTION_POSTS_DATABASE; const posts = await getPostsPath(notionDatabaseID!); await postNav.register(posts); const paths = posts.map((post) => ({ params: { id: post.postId }, })); return { paths, fallback: false }; }; export const getStaticProps: GetStaticProps = async ({ params }) => { if (!params || typeof params.id !== 'string') { return { redirect: { destination: '/', permanent: false, }, }; } const { recordMap, resultPost: post } = await getDetailPost(params.id); const nav = await postNav.get(); return { props: { recordMap, post, nav, }, revalidate: 10, }; };
// @/lib/index.js export const postNav = { register: async (posts) => { return await fs.writeFile( path.join(__dirname, 'postIDs.db'), JSON.stringify(posts.map(({ postId }) => postId).reverse()), ); }, get: async () => { const postIDs = await fs.readFile(path.join(__dirname, 'postIDs.db')); const list = JSON.parse(postIDs); return list; }, };
lib 폴더에 내부 API를 만들어두고 getStaticPaths에서 cache파일을 쓰고, getStaticProps에서 cache파일을 읽는다.
SSG인 블로그는 build시 전체 파일이 초기화되므로 cache파일을 삭제하지 않아도 문제가 없다.
기본적인 SSG로는 캐시를 통해 전달하면 된다. 하지만 현재 프로젝트는 ISR을 적용하고 있어서, 포스트가 첫 빌드되고 나서도 revalidate에 따라 지속적으로 re-build가 일어난다. 매번 cache파일을 다시 쓰는 작업이 발생하는 것은 대형 프로젝트일 경우에 서버에 무리가 될거라 예상된다.
실제로 동작해본 결과 local production 서버에서는 잘 동작하나 vecel 에서는 에러 500으로 동작하지 않는다. ISR에서는 동작하지 못하는 것이다.
문제의 문제를 해결하는 법
그럼 어떻게 해결할 수 있을까?
첫 째, revalidate의 시간을 보수적으로 잡아 컴퓨터의 부담을 줄여줄 수도 있다. 하지만 이것은 근본적인 해결책이 될 수 없다. 초대형의 서버 관점에서 수많은 포스트들은 끊임없이 re-build가 일어날 것이기 때문이다.
두 번째는, memory cache를 이용하는 법이다
cacache 같은 라이브러리를 통해 메모리로 캐시함으로서 파일 캐시에 비해 성능향상을 기대할 수 있을 것이다
세 번째는, 최소화된 API 설계를 통해 getStaticProps에서 데이터를 처리하는 것이다.
기본적으로는 이렇게 처리하면 될 것이기 때문에 NEXT 진영에서도 두 함수간의 데이터 전달을 위한 뚜렷한 해결책이 나오지 않은 거라 생각한다. getStaticProps는 node worker에서 동작하고 getStaticPath가 동작하는 프로세스와는 다르기 때문에 데이터가 연결되기 힘들다고 한다.
프로젝트는 세번째 방법을 이용하여 적용해두었다. NEXT의 개선이 조금 더딘것 같다. 신입인 나를 쓰면 반값으로 일해서 해결책을 만들어둘텐데…