2 글 보임 - 1 에서 2 까지 (총 2 중에서)
-
글쓴이글
-
2023년 6월 10일 16:49 #86508
아왜나만갖고그래참가자next.js 13버전으로 블로그를 개발해보고 있습니다. layout.tsx에는 유저가 선택한 테마정보를 가져올 수 있는 코드를 짜놓은 상태입니다.
// layout.tsx
import Navbar from './Navbar'; import Footer from './Footer'; import { cookies } from 'next/headers'; import GrainFilter from './GrainFilter';
export const Pretendard = localFont({ src: '../../public/fonts/PretendardVariable.woff2', });
export default function RootLayout({ children }: { children: React.ReactNode }) { const cookiesStore = cookies(); const theme = cookiesStore.get('theme')?.value; const bodyTheme = theme === 'dark' ? 'dark' : 'light';
return ( <html lang="ko"> <body className={Pretendard.className} data-theme={bodyTheme}> <GrainFilter /> <Navbar /> <main>{children}</main> <Footer /> </body> </html> ); }
그리고 블로그 포스트의 글을 보여주는 곳에는 아래처럼 짜놨습니다.
// src/app/post/[slug]/page.tsx
import { fontMono } from '@/app/layout'; import { allPosts } from 'contentlayer/generated'; import dayjs from 'dayjs'; import { getMDXComponent } from 'next-contentlayer/hooks'; import Aside from './Aside'; import styles from './page.module.css'; import { filterNonDraft, scrollToTop } from '@/app/util'; import Link from 'next/link'; import { CalendarIcon, LeftAngleIcon, ListIcon, RightAngleIcon, TagIcon, TimerIcon, UndoIcon, UpIcon, } from '@/styles/svgIcons'; import Pre from '@/components/mdx-components/Pre';
export async function generateStaticParams() { return allPosts.map(post => ({ slug: post._raw.flattenedPath.split('/')[1], })); } /* 위 코드를 콘솔에 출력하면 다음과 같이 출력됨
[ { slug: 'test1' }, { slug: 'test2' }, { slug: 'test3' }, { slug: 'test4' }, { slug: 'test5' }, { slug: 'test6' }, { slug: 'test7' }, { slug: 'test8' }, { slug: 'test9' }, ]
*/
export default function PostLayout({ params }: { params: { slug: string } }) { if (!params) { // 체크하기 return <div>?</div>; }
// 현재 포스트 정보를 가져오기 위한 코드들 const post = allPosts.find( post => post._raw.flattenedPath.split('/')[1] === params.slug ); // 여기서 post가 undefined일 경우 Content가 비정의되는 문제 해결 if (!post) throw new Error(`Post not found for slug: ${params.slug}`); const MDXLayout = getMDXComponent(post.body.code);
const formattedDate = dayjs(post.date).format('YYYY. MM. DD');
/* - contentlayer.config.ts 설정대로 mdx 파일이 변환됨. - 클릭한 글과 매치되는 파일을 찾아 그걸 post에 할당함. - 할당된 post 정보는 .contentlayer 폴더 안에 존재. - post.body.code 부분을 추출해서 Content에 할당. 이 부분은 완전한 JS 코드로 이뤄짐 - 이걸 컴포넌트로 넣으면 서버에서는 js 코드를 html로 변환함. */
// 이전 포스트, 다음 포스트를 위한 코드들 const posts = filterNonDraft(allPosts).sort((a, b) => dayjs(b.date).diff(a.date)); const currentPostIdx = posts.findIndex(el => el.title === post.title); const prevPost = posts[currentPostIdx - 1]; const nextPost = posts[currentPostIdx + 1];
const components = { pre: Pre, };
return ( <> <section className="sub-header post-title"> <h1>{post.title}</h1> <div className={styles.infoBox}> <CalendarIcon width={12} style={{ marginRight: '5px' }} /> <span className="small-info"> <time dateTime={post.date}>{formattedDate}</time> </span> <TimerIcon width={12} style={{ marginRight: '5px' }} /> <span className="small-info">{post.readTimeMinutes}분</span> </div> </section> <article className="main-section content-area"> {/* <Aside headings={post.headings} params={params} title={post.title} /> */} <MDXLayout className={fontMono.className} components={components} /> </article> <section> <div className={styles.articleFooter}> <div className={styles.tagList}> <TagIcon width={16} style={{ marginRight: '7px' }} /> {post.tags?.map((el, idx) => ( <span className={styles.tagElement} key={idx}> {el} </span> ))} </div> <div> <Link href="/post" aria-label="목록으로" className={`${styles.backToList} tooltip`} > <UndoIcon width={16} /> </Link> </div> </div> <div className={styles.pagination} style={!prevPost ? { flexFlow: 'row-reverse' } : undefined} > {prevPost ? ( <Link href={`/${prevPost.url}`} className={styles.prevPagination}> <LeftAngleIcon width={36} style={{ marginRight: '5px' }} /> <div> <h5>이전 글</h5> <h3>{prevPost.title}</h3> </div> </Link> ) : undefined} {nextPost ? ( <Link href={`/${nextPost.url}`} className={styles.nextpagination}> <div> <h5>다음 글</h5> <h3>{nextPost.title}</h3> </div> <RightAngleIcon width={36} style={{ marginLeft: '5px' }} /> </Link> ) : undefined} </div> </section> <section className={styles.comment}> <span>댓글</span> </section> </> ); } 해당 페이지의 사이드에서 같이 따라다니는 목차인 Aside.tsx는 아래처럼 만들었습니다.
'use client';
import { useEffect, useState } from 'react'; import styles from './page.module.css'; import { UpIcon } from '@/styles/svgIcons'; import { scrollToTop } from '@/app/util'; import { useScrollTop } from '@/hooks/useScrollTop';
type heading = { level: number; text: string; slug: string; };
type AsideProps = { headings: heading[]; params: { slug: string }; title: string; };
export default function Aside({ headings, params, title }: AsideProps) { const isScrolled = useScrollTop(); const marginReturn = (level: number) => { switch (level) { case 1: // 가장 상위 헤더이므로 마진을 주지 않음 break; case 2: return { marginLeft: '10px' }; case 3: return { marginLeft: '20px' }; case 4: case 5: case 6: return { marginLeft: '30px' }; } };
let anchorPositions: number[] = []; const [headingBold, setHeadingBold] = useState<boolean[]>([]);
useEffect(() => { function calculateAnchorPositions() { const anchor = document.querySelectorAll('.anchor'); let anchorLocation = [];
if (!anchor) return;
for (let i = 0; i < anchor.length; i++) { anchorLocation.push(anchor[i].getBoundingClientRect().top + window.scrollY); }
anchorPositions = anchorLocation; }
calculateAnchorPositions();
window.addEventListener('scroll', calculateAnchorPositions);
return () => { window.removeEventListener('scroll', calculateAnchorPositions); }; }, []);
useEffect(() => { const handleScroll = () => { const currentYOffset = window.scrollY; const boldMapping = headings.map(() => false);
for (let i = 0; i < headings.length; i++) { if ( anchorPositions[i] - 95 - currentYOffset < 0 && (i + 1 >= anchorPositions.length || anchorPositions[i + 1] - 95 - currentYOffset >= 0) ) { boldMapping[i] = true; break; } }
setHeadingBold(boldMapping); };
handleScroll(); window.addEventListener('scroll', handleScroll);
return () => { window.removeEventListener('scroll', handleScroll); }; }, [headings]);
return ( <> <aside className={styles.tocWrapper}> <div className={styles.toc}> <h3>{title}</h3> {headings.map((heading: heading, idx) => { return ( <div key={`#${heading.slug}`} style={marginReturn(heading.level)}> <a href={`${params.slug}#${heading.slug}`} className={headingBold[idx] ? styles.tocBold : undefined} > {heading.text} </div> ); })} <div className={styles.tocFooter}> <button className="round-btn" onClick={scrollToTop}> <UpIcon width="16px" /> </button> </div> </div> </aside> <button className={`round-btn fixed-btn ${isScrolled ? 'visible' : 'invisible'}`} onClick={scrollToTop} > <UpIcon width={16} /> </button> </> ); }
홈 화면, 다른 화면에서는 아무 문제가 없는데, 유독 포스트 상세 페이지에 들어가는 순간 다음과 같이 에러가 나옵니다.
Unhandled Runtime Error Error: Dynamic server usage: cookies
Source staticGenerationBailout node_modules\next\dist\client\components\static-generation-bailout.js (33:20) cookies node_modules\next\dist\client\components\headers.js (45:62) src\app\layout.tsx (31:30) @ cookies
29 | 30 | export default function RootLayout({ children }: { children: React.ReactNode }) { > 31 | const cookiesStore = cookies(); | ^ 32 | const theme = cookiesStore.get('theme')?.value; 33 | const bodyTheme = theme === 'dark' ? 'dark' : 'light'; 34 | Call Stack type node_modules\next\dist\compiled\react-server-dom-webpack\cjs\react-server-dom-webpack-server.edge.development.js (1447:17) attemptResolveElement node_modules\next\dist\compiled\react-server-dom-webpack\cjs\react-server-dom-webpack-server.edge.development.js (1759:20) resolveModelToJSON node_modules\next\dist\compiled\react-server-dom-webpack\cjs\react-server-dom-webpack-server.edge.development.js (1249:13) stringify <anonymous> stringify node_modules\next\dist\compiled\react-server-dom-webpack\cjs\react-server-dom-webpack-server.edge.development.js (181:13) processModelChunk node_modules\next\dist\compiled\react-server-dom-webpack\cjs\react-server-dom-webpack-server.edge.development.js (2062:25) retryTask node_modules\next\dist\compiled\react-server-dom-webpack\cjs\react-server-dom-webpack-server.edge.development.js (2109:6) performWork node_modules\next\dist\compiled\react-server-dom-webpack\cjs\react-server-dom-webpack-server.edge.development.js (1544:13) listOnTimeout node:internal/timers (569:17) process.processTimers node:internal/timers (512:7) Hide collapsed frames
근데 여기서 page.tsx의 generateStaticParams 부분을 주석처리하면 이상하게 정상적으로 들어갈 수 있습니다.
문제는 이 상태로 빌드해서 Vercel에 배포하니 포스트 리스트 페이지에 들어가자마자 아래와 같은 에러가 뜨고 접속이 안됩니다..
Uncaught (in promise) SyntaxError: Unexpected token '통', ..."0\\uB97C \통\해 \\uC5B"... is not valid JSON at JSON.parse (<anonymous>) at 5449 (page-b5328370f0a03e92.js:1:266) at Function.d (webpack-650338c4d8aad33a.js:1:152) 488-2c6b78f8e437145e.js:1 SyntaxError: Unexpected token '통', ..."0\\uB97C \통\해 \\uC5B"... is not valid JSON at JSON.parse (<anonymous>) at 5449 (page-b5328370f0a03e92.js:1:266) at d (webpack-650338c4d8aad33a.js:1:152) at w (488-2c6b78f8e437145e.js:9:4271) at O (488-2c6b78f8e437145e.js:9:2855) at iv (2443530c-824e93370d7398c3.js:9:112213) at oR (2443530c-824e93370d7398c3.js:9:90030) at 2443530c-824e93370d7398c3.js:9:86288 at 2443530c-824e93370d7398c3.js:9:86295 at ok (2443530c-824e93370d7398c3.js:9:86400) window.console.error @ 488-2c6b78f8e437145e.js:1
구글링을 통해 찾아낸 답변인 layout.tsx에 export const dynamic = 'force-dynamic' 을 추가하는 방법을 적용해도 여전히 Dynamic server usage 오류가 발생합니다.
2023년 6월 10일 20:38 #86529
codingapple키 마스터generateStaticParams 쓰면 static 렌더링되고 cookies 쓰면 dynamic 렌더링되어서 둘 중 하나만 써야할듯요
-
글쓴이글
2 글 보임 - 1 에서 2 까지 (총 2 중에서)
- 답변은 로그인 후 가능합니다.