• 로그인
  • 장바구니에 상품이 없습니다.

home2 게시판 Next.js 게시판 layout.tsx에 cookies()가 특정 페이지 접속시 오류가 나는 현상

layout.tsx에 cookies()가 특정 페이지 접속시 오류가 나는 현상

2 글 보임 - 1 에서 2 까지 (총 2 중에서)
  • 글쓴이
  • #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 오류가 발생합니다.
    
    #86529

    codingapple
    키 마스터
    generateStaticParams 쓰면 static 렌더링되고 cookies 쓰면 dynamic 렌더링되어서 
    둘 중 하나만 써야할듯요
2 글 보임 - 1 에서 2 까지 (총 2 중에서)
  • 답변은 로그인 후 가능합니다.

About

현재 월 700명 신규수강중입니다.

  (09:00~20:00) 빠른 상담은 카톡 플러스친구 코딩애플 (링크)
  admin@codingapple.com
  이용약관
ⓒ Codingapple, 강의 예제, 영상 복제 금지
top

© Codingapple, All rights reserved. 슈퍼로켓 에듀케이션 / 서울특별시 강동구 고덕로 19길 30 / 사업자등록번호 : 212-26-14752 온라인 교육학원업 / 통신판매업신고번호 : 제 2017-서울강동-0002 호 / 개인정보관리자 : 박종흠