인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

Inflearn Community Q&A

인생꿀잼's profile image
인생꿀잼

asked

Next + Create a SNS service with React Query

hydration 오류 (Fragment key값)

Resolved

Written on

·

48

0

강사님 안녕하세요, 백엔드 서버 연결하고 게시물(PostForm.tsx) 올리는 부분에서 오류가 발생하여 질문드립니다. (섹션5 - 게시글 업로드 완성)

hydration 이슈를 찾아봤는데 (https://velog.io/@jhplus13/NextJS-React-Hydration-Error-%ED%95%B4%EA%B2%B0%EA%B8%B0) typeof window과 관련있는거 같은데 혼자 해결하기 어려워 링크 첨부드립니다..!

오류해결을 위해 (hydration / PostRecommends.tsx) 해당 부분을 수정하였습니다..!

  1. 변수입력확인(Date.now(),

    Math.random(),

    new Date()) + faker 해당부분은 주석처리 했습니다.

  2. 태그 중첩 확인, layout.tsx font 연결 확인

     

[콘솔 에러]

intercept-console-error.js:56 A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

https://react.dev/link/hydration-mismatch

  ...
    <HotReload assetPrefix="">
      <ReactDevOverlay state={{nextId:1, ...}} dispatcher={{...}}>
        <DevRootNotFoundBoundary>
          <NotFoundBoundary notFound={<NotAllowedRootNotFoundError>}>
            <NotFoundErrorBoundary pathname="/home" notFound={<NotAllowedRootNotFoundError>} notFoundStyles={undefined} ...>
              <RedirectBoundary>
                <RedirectErrorBoundary router={{...}}>
                  <Head>
                  <link>
                  <RootLayout>
                    <html lang="ko" className="__variable...">
                      <body
                        className="__className_fde3a9"
-                       cz-shortcut-listen="true"
                      >
                  ...
        ...

[Fragment key값 오류내용]

image.png
// package.json
{
  "name": "zcom",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@auth/core": "^0.27.0",
    "@fortawesome/fontawesome-svg-core": "^6.7.1",
    "@fortawesome/free-solid-svg-icons": "^6.7.1",
    "@fortawesome/react-fontawesome": "^0.2.2",
    "@tanstack/react-query": "^5.62.11",
    "@vanilla-extract/css": "^1.16.1",
    "@vanilla-extract/recipes": "^0.5.5",
    "@vanilla-extract/sprinkles": "^1.6.3",
    "classnames": "^2.5.1",
    "clsx": "^2.1.1",
    "dayjs": "^1.11.13",
    "next": "^15.0.4",
    "next-auth": "^5.0.0-beta.25",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-intersection-observer": "^9.14.1",
    "react-textarea-autosize": "^8.5.6",
    "zustand": "^5.0.2"
  },
  "devDependencies": {
    "@faker-js/faker": "^9.3.0",
    "@tanstack/react-query-devtools": "^5.62.11",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@vanilla-extract/next-plugin": "^2.4.7",
    "@vanilla-extract/webpack-plugin": "^2.3.15",
    "msw": "^2.6.8",
    "typescript": "^5"
  },
  "msw": {
    "workerDirectory": [
      "public"
    ]
  }
}
reactnext.jsreact-querynext-authmsw

Answer 3

0

인생꿀잼님의 프로필 이미지
인생꿀잼
Questioner

세팅 문제 같아 백엔드 서버 세팅 다시 하고 실행해 봤을때 (nest-prisma파일 1월 6일 기준) postForm부분에서 /api/posts에서 500 에러가 발생하고 있습니다..! user 정보(authjs.session-token)도 잘 받아오고 있습니다..ㅠㅜ

image.pngimage.png
// 백엔드 터미널 오류 내용
canActivate with req.user undefined
eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoieFQ0MjR1azFqQi1xRlFINjRfaVY0OE4tRHRNbzh1b3JHSHZENnhVRFI1Tm5Za3JYRHBGbU5yZlVfd3Z4TmUzT2lqUUdVWFhTYkhGZXYtM295YVM0QUEifQ..W_hVRdWsTS-VthkfaJClwA.FO9GzpLUvS7u13TGyAU0NOm3T31wpThqCsKw6DGWTNiLrB9qPojT46wzKSprfxdphVwaTtuU_T_PWPfKOeQ4CerC1UhmqnCB2NpDdvaqy8dzQhx04DluwW3gtZwsMnsG5s3Y0XV76FBOZ0NyeNFApg0wktotHD-C7bawlWTVYySjr4DI0QfAq0zcLUTJYvJt8eRoicHkYynka9RJaJnqnKESDDfbRt_zHeykblhbRaE.DBUwZAHepn3XCyZgJioJQZCIUQ8zs5GeO5Y0wnY2jkM
[Nest] 33332  - 2025. 01. 06. 오후 4:49:28   ERROR [ExceptionsHandler] no matching decryption secret
Error: no matching decryption secret
    at jwtDecrypt.clockTolerance (file:///C:/Users/alsdu/Desktop/nest-prisma-main/node_modules/@auth/core/jwt.js:76:15)
    at flattenedDecrypt (file:///C:/Users/alsdu/Desktop/nest-prisma-main/node_modules/jose/dist/node/esm/jwe/flattened/decrypt.js:92:15)
    at compactDecrypt (file:///C:/Users/alsdu/Desktop/nest-prisma-main/node_modules/jose/dist/node/esm/jwe/compact/decrypt.js:15:23)
    at jwtDecrypt (file:///C:/Users/alsdu/Desktop/nest-prisma-main/node_modules/jose/dist/node/esm/jwt/decrypt.js:5:23)
    at decode (file:///C:/Users/alsdu/Desktop/nest-prisma-main/node_modules/@auth/core/jwt.js:67:25)
    at LoggedInGuard.canActivate (C:\Users\alsdu\Desktop\nest-prisma-main\src\auth\logged-in-guard.ts:17:23)
    at GuardsConsumer.tryActivate (C:\Users\alsdu\Desktop\nest-prisma-main\node_modules\@nestjs\core\guards\guards-consumer.js:16:17)
    at canActivateFn (C:\Users\alsdu\Desktop\nest-prisma-main\node_modules\@nestjs\core\router\router-execution-context.js:134:33)
    at C:\Users\alsdu\Desktop\nest-prisma-main\node_modules\@nestjs\core\router\router-execution-context.js:42:31
    at C:\Users\alsdu\Desktop\nest-prisma-main\node_modules\@nestjs\core\router\router-proxy.js:9:17
[Nest] 33332  - 2025. 01. 06. 오후 4:49:28     LOG [HTTP] POST /api/posts 500 52 - Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 ::1
// .env
NEXT_PUBLIC_BASE_URL=http://localhost:9090
AUTH_SECRET=비번
NEXT_PUBLIC_MODE=local
NEXT_PUBLIC_MSW_ENABLED=false

// postForm.tsx(onSubmit)
const onSubmit: FormEventHandler = async (e) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append('content', content);
    preview.forEach((p) => {
      p && formData.append('images', p.file);
    })
    try{
      const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/posts`, {
        method: 'post',
        credentials: 'include',
        body: formData,
      });
      if (response.status === 201) {
        setContent('');
        setPreview([]);
        const newPost = await response.json();
        queryClient.setQueryData(['posts', 'recommends'], (prevData: { pages: Post[][] }) => {
          const shallow = {
            ...prevData,
            pages: [...prevData.pages],
          };
          shallow.pages[0] = [...shallow.pages[0]];
          shallow.pages[0].unshift(newPost);
          return shallow;
        });
        queryClient.setQueryData(['posts', 'recommends'], (prevData: { pages: Post[][] }) => {
          const shallow = {
            ...prevData,
            pages: [...prevData.pages],
          };
          shallow.pages[0] = [...shallow.pages[0]];
          shallow.pages[0].unshift(newPost);
          return shallow;
        });
        queryClient.setQueryData(['posts', 'followings'], (prevData: { pages: Post[][] }) => {
          const shallow = {
            ...prevData,
            pages: [...prevData.pages],
          };
          shallow.pages[0] = [...shallow.pages[0]];
          shallow.pages[0].unshift(newPost);
          return shallow;
        });
      }
    } catch(err){
      alert('업로드 중 에러발생');
    }
  };
zerocho님의 프로필 이미지
zerocho
Instructor

백엔드.env에도 AUTH_SECRET 넣어주세요!

인생꿀잼님의 프로필 이미지
인생꿀잼
Questioner

감사합니다! 백앤드/프론트 .env AUTH_SECRET가 달랐더라구요..!ㅠㅜ

0

zerocho님의 프로필 이미지
zerocho
Instructor

이 경우는 먼저 index값을 console.log 해봐야 할 것 같습니다. 중복된 키가 오는 게 말이 안 되긴 한데 희한하네요

0

안녕하세요, 인프런 AI 인턴입니다.

인생꿀잼님께서 제기하신 하이드레이션 오류와 관련하여, 이는 주로 서버와 클라이언트 사이에 렌더링된 HTML이 일치하지 않을 때 발생하는 문제입니다. 이 문제가 발생하는 주된 원인에는 Date.now(), Math.random(), new Date() 등의 동적 데이터 사용과 클라이언트 사이드에서만 접근 가능한 window 객체의 사용이 포함됩니다.

해결 방법 요약

  1. 동적 데이터 고정: Date.now(), Math.random() 등의 함수를 서버와 클라이언트에서 동일한 값을 생성하도록 고정해야 합니다. 예를 들어, faker를 사용할 때는 faker.seed(123)과 같이 시드를 고정하면 랜덤 생성이 일관되게 됩니다.

  2. 키 프로퍼티 설정: React에서 리스트를 렌더링할 때는 각 자식 요소에 고유한 key를 부여해야 합니다. 이때 key로 인덱스를 사용하는 것은 피하고, 가능한 unique한 식별자를 사용하는 것이 좋습니다. 아래의 작성하신 코드를 기반으로:

    data?.pages.map((page) => (
        <Fragment key={page.someUniqueId}>  // 여기서 someUniqueId는 page 별 고유ID로 변경
            {page.posts.map((post) => (
                <Post key={post.postId} post={post} />
            ))}
        </Fragment>
    ));
    

    page 객체에서 고유한 식별자를 찾아 Fragmentkey로 설정하세요.

  3. typeof window 사용 검토: 서버 사이드 렌더링(SSR)에서는 클라이언트 전용 객체인 window에 접근할 수 없습니다. if (typeof window !== 'undefined')와 같은 조건문으로 클라이언트에서만 실행될 코드를 분리하세요.

더불어, 이 링크에서 제안된 방법은 @faker-js/faker 사용 시 하이드레이션 에러를 해결하는데 참고할 수 있을 것입니다. faker.seed(123)로 랜덤 데이터를 고정하여 서버와 클라이언트의 불일치를 방지한 사례입니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.

현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏

추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

인생꿀잼's profile image
인생꿀잼

asked

Ask a question