인프런 커뮤니티 질문&답변

i1004gy님의 프로필 이미지

작성한 질문수

[리뉴얼] React로 NodeBird SNS 만들기

서버사이드렌더링 준비하기

toolkit을 사용 ssr설정 질문입니다

해결된 질문

23.08.10 17:43 작성

·

306

0

https://github.com/ZeroCho/react-nodebird/blob/master/toolkit/front/pages/index.js

여기 코드를 가져와서 ssr을 설정했습니다

front 코드 에러로

Error: Hydration failed because the initial UI does not match what was rendered on the server.

Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

이렇게 두개가 나오는데 이걸 어떻게 해결할지 잘 모르겠습니다 initaial UI 에러라길레

initialState: {
  user: {
    ...userInitialState,
    me: myInfo,
  },
  post: {
    ...postInitialState,
    mainPosts: posts,
    hasMorePosts: posts.length === 10,
  },
},

주석 처리 되어있는 이부분을 어떻게 해야되는거 같은데 잘 모르겠습니다

답변 2

0

i1004gy님의 프로필 이미지
i1004gy
질문자

2023. 08. 14. 14:57

reducers/index.js

import { combineReducers } from "redux";
import { HYDRATE } from "next-redux-wrapper";
import axios from "axios";

import userSlice from "./user";
import postSlice from "./post";

axios.defaults.baseURL = "http://localhost:3065";
axios.defaults.withCredentials = true;

// (이전상태, 액션) => 다음상태
const rootReducer = (state, action) => {
  switch (action.type) {
    case HYDRATE:
      console.log("HYDRATE", action);
      return action.payload;
    default: {
      const combinedReducer = combineReducers({
        user: userSlice.reducer,
        post: postSlice.reducer,
      });
      return combinedReducer(state, action);
    }
  }
};

export default rootReducer;

오류 해결했습니다 index.js에서 하이드레이트 하는 부분의 문법이 조금 달랐네요!

https://kir93.tistory.com/entry/NextJS-Redux-Toolkit-next-redux-wrapper-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0

이 링크가 도움이 됐습니다!!

0

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

2023. 08. 10. 18:09

이 에러는 어떠한 이유에서든지 서버에서 렌더링한 것과 브라우저에서 렌더링한 것이 일치하지 않아서 발생하는 문제입니다. 사실 무시하셔도 되는 에러이기는 합니다. 해결하려고 할 때는 내 정보가 들어있는 경우 해결하기 쉽지가 않습니다. 애초에 내 정보는 ssr하지 않는 것이 좋습니다. 내 정보는 브라우저에서 따로 불러오세요.

i1004gy님의 프로필 이미지
i1004gy
질문자

2023. 08. 10. 22:11

내 정보가 들어있는 경우라는게 정확하게 어떤 경우인지 잘이해가 되질 않습니다. 내 정보라는게 정확이 무었을 말씀하시는 건가요?

또 내 정보는 ssr하지 않는 것이 좋다고 하셨는데 구체적인 방법이 어떻게 되나요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

2023. 08. 10. 22:29

myInfo 입니다. ssr하지말고 useEffect에서 loadMyInfo 하시면 됩니다.

i1004gy님의 프로필 이미지
i1004gy
질문자

2023. 08. 10. 23:00

import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import axios from "axios";

import PostForm from "../components/PostForm";
import PostCard from "../components/PostCard";
import AppLayout from "../components/AppLayout";

import wrapper from "../store/configureStore";
import { loadPosts } from "../reducers/post";
import { loadMyInfo } from "../reducers/user";
const Home = (props) => {
  console.log("props", props);
  const { me } = useSelector((state) => state.user);
  const { mainPosts, hasMorePost, loadPostsLoading, retweetError } =
    useSelector((state) => state.post);

  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(loadMyInfo());
  }, []);
  useEffect(() => {
    if (retweetError) {
      alert(retweetError);
    }
  }, [retweetError]);

  const lastId = mainPosts[mainPosts.length - 1]?.id;
  useEffect(() => {
    function onScroll() {
      console.log(
        window.scrollY,
        document.documentElement.clientHeight,
        document.documentElement.scrollHeight
      );
      if (
        window.scrollY + document.documentElement.clientHeight >
        document.documentElement.scrollHeight - 300
      )
        if (hasMorePost && !loadPostsLoading) {
          dispatch(loadPosts(lastId));
        }
    }
    window.addEventListener("scroll", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
    };
  }, [hasMorePost, mainPosts]);

  return (
    <AppLayout>
      {me && <PostForm />}
      {mainPosts.map((post) => {
        return <PostCard key={post.id} post={post} />;
      })}
    </AppLayout>
  );
};

// SSR (프론트 서버에서 실행)
export const getServerSideProps = wrapper.getServerSideProps(
  (store) =>
    async ({ req }) => {
      const cookie = req ? req.headers.cookie : "";
      axios.defaults.headers.Cookie = "";
      // 쿠키가 브라우저에 있는경우만 넣어서 실행
      // (주의, 아래 조건이 없다면 다른 사람으로 로그인 될 수도 있음)
      if (req && cookie) {
        axios.defaults.headers.Cookie = cookie;
      }

      await store.dispatch(loadPosts());

      return {
        props: {
          // initialState: {
          //   user: {
          //     ...userInitialState,
          //     me: myInfo,
          //   },
          //   post: {
          //     ...postInitialState,
          //     mainPosts: posts,
          //     hasMorePosts: posts.length === 10,
          //   },
          // },
        },
      };
    }
);

export function reportWebVitals(metric) {
  console.log(metric);
}

export default Home;

이런식으로 ssr외부로 loadMyinfo를 밖으로 빼라는 말씀이신가요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

2023. 08. 10. 23:43

네 맞습니다.

i1004gy님의 프로필 이미지
i1004gy
질문자

2023. 08. 11. 16:00

이런식으로 loadMyInfo를 밖으로 빼도 똑같은 에러가 발생합니다 그리고 loadMyInfo를 ssr하지 말라는 말씀은 새로고침해도 로그인이 풀렸다 다시 로그인이 되는 현상으로 돌아가라는 말씀이신가요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

2023. 08. 11. 16:04

네, 그냥 유저 정보를 불러올 때 로딩창처럼 띄워서 가리고 나서 유저 정보를 받아오는 것이 낫습니다. 검색 엔진에 나와야하는 데이터가 아니라면 굳이 ssr할 필요가 없습니다.

말씀드렸던것처럼 이 에러는 해결하기가 쉽지 않습니다. 어느 부분에서 서버와 브라우저가 렌더링한 게 달라지는 건지부터 찾아야 합니다.

https://github.com/vercel/next.js/discussions/35773#discussioncomment-2840696

여기에서처럼 수많은 원인들이 있어서 하나씩 다 찾아보아야 합니다. 제일 좋은 건 저 에러가 발생하지 않았던 상황으로 돌아가서 다시 하나씩 추가해보면서 언제부터 발생하는지 찾는 겁니다.

i1004gy님의 프로필 이미지
i1004gy
질문자

2023. 08. 11. 17:16

그렇군요 그럼 궁금한게 서버사이드 랜더링은 요청이 들어오면 서버에서 페이지를 만들어서 프론트로 보내주는것이라고 이해하고 있는데 서버에서 랜더링한것과 브라우저에서 랜더링 한것이 다르다는 것이 어떻게 발생할 수 있나요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

2023. 08. 11. 17:20

브라우저에서 ssr된 html을 받으면서 리액트 트리를 구성합니다. 왜냐면 앞으로는 브라우저 react가 서버에서 온 데이터를 넘겨받아서 처리하기 때문입니다. 그게 hydration이라는 과정이고요. 이 때 서버에서 온 데이터와 브라우저에서 구성한 트리가 다르게 되면 지금같은 문제가 발생합니다. 예를 들어 Math.random()같은 걸 쓰면 서버랑 브라우저랑 달라지게 됩니다.

 

그래서 생각난 건데 혹시 지금도 faker 쓰고 계신가요? 이게 랜덤 데이터이긴 합니다.

i1004gy님의 프로필 이미지
i1004gy
질문자

2023. 08. 11. 18:46

아니요 지금 faker는 사용하고 있지 않습니다

i1004gy님의 프로필 이미지
i1004gy
질문자

2023. 08. 11. 18:48

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import axios from 'axios';

import AppLayout from '../components/AppLayout';
import PostForm from '../components/PostForm';
import PostCard from '../components/PostCard';
import { loadMyInfo } from '../reducers/user';
import { loadPosts } from '../reducers/post';
import wrapper from '../store/configureStore';

const Home = (props) => {
  console.log('props', props);
  const dispatch = useDispatch();
  const { me } = useSelector((state) => state.user);
  const { mainPosts, hasMorePosts, loadPostsLoading, retweetError } = useSelector((state) => state.post);

  useEffect(() => {
    if (retweetError) {
      alert(retweetError);
    }
  }, [retweetError]);

  const lastId = mainPosts[mainPosts.length - 1]?.id;
  useEffect(() => {
    function onScroll() {
      if (window.pageYOffset + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) {
        if (hasMorePosts && !loadPostsLoading) {
          dispatch(loadPosts(lastId));
        }
      }
    }

    window.addEventListener('scroll', onScroll);
    return () => {
      window.removeEventListener('scroll', onScroll);
    };
  }, [hasMorePosts, loadPostsLoading, mainPosts]);
  return (
    <AppLayout>
      {me && <PostForm />}
      {mainPosts.map((post) => <PostCard key={post.id} post={post} />)}
    </AppLayout>
  );
};

// SSR (프론트 서버에서 실행)
export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req }) => {
  const cookie = req ? req.headers.cookie : '';
  axios.defaults.headers.Cookie = '';
  // 쿠키가 브라우저에 있는경우만 넣어서 실행
  // (주의, 아래 조건이 없다면 다른 사람으로 로그인 될 수도 있음)
  if (req && cookie) {
    axios.defaults.headers.Cookie = cookie;
  }
  await store.dispatch(loadPosts());
  await store.dispatch(loadMyInfo());

  return {
    props: {
      // initialState: {
      //   user: {
      //     ...userInitialState,
      //     me: myInfo,
      //   },
      //   post: {
      //     ...postInitialState,
      //     mainPosts: posts,
      //     hasMorePosts: posts.length === 10,
      //   },
      // },
    },
  };
});

export function reportWebVitals(metric) {
  console.log(metric);
}

export default Home;

위 코드에서 props에 주석처리 되어있는 부분은 왜 주석처리 해놓은건지 알 수 있을까요?

제가 chat gpt로도 검색을 좀 해봤는데 더미데이터로 데이터 형식을 넘겨줘야한다는 식으로 답변이 와서 혹시 저부분이 문제인가 싶어서 그렇습니다

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

2023. 08. 11. 18:50

저도 기억이 잘 안 나는데 필요없는 코드입니다.

i1004gy님의 프로필 이미지
i1004gy
질문자

2023. 08. 11. 19:05

오류가 일어나는 위치는

await store.dispatch(loadPosts()); 
await store.dispatch(loadMyInfo());

이 부분입니다 두개 모두에서 일어납니다 하나씩 지워봤는데 두개 모두에게서 일어났습니다

i1004gy님의 프로필 이미지
i1004gy
질문자

2023. 08. 11. 19:12

front/reducers/posts.js

import {
  createSlice,
  createAsyncThunk,
  startListening,
  createListenerMiddleware,
} from "@reduxjs/toolkit";
import { throttle } from "lodash";
import axios from "axios";
import { HYDRATE } from "next-redux-wrapper";
export const initialState = {
  mainPosts: [],
  imagePaths: [],
  hasMorePost: true,
  addPostLoading: false,
  addPostDone: false,
  addPostError: null,
  addCommentLoading: false,
  addCommentDone: false,
  addCommentError: null,
  addRemoveLoading: false,
  addRemoveDone: false,
  addRemoveError: null,
  loadPostsLoading: false,
  loadPostsDone: false,
  loadPostsError: null,
  likePostLoading: false,
  likePostDone: false,
  likePostError: null,
  unlikePostLoading: false,
  unlikePostDone: false,
  unlikePostError: null,
  removePostLoading: false,
  removePostDone: false,
  removePostError: null,
  uploadImagesLoading: false,
  uploadImagesDone: false,
  uploadImagesError: null,
  retweetLoading: false,
  retweetDone: false,
  retweetError: null,
};
export const loadPosts = createAsyncThunk("/loadposts", async (lastId) => {
  throttledFetchData();
  const response = await axios.get(`/posts?lastId=${lastId || 0}`);
  return response.data;
});
const postSlice = createSlice({
  name: "post",
  initialState,
  reducers: {
    // 비동기 액션이기 때문에 async를 설정안해도 된다
    removeImage(state, action) {
      state.imagePaths = state.imagePaths.filter(
        (v, i) => i !== action.payload
      );
    },
  },
  extraReducers: (builder) =>
    builder
      .addCase([HYDRATE], (state, action) => ({
        ...state,
        ...action.payload.post,
      }))
      // loadPosts
      .addCase(loadPosts.pending, (state, action) => {
        console.log(action);
        state.loadPostsLoading = true;
        state.loadPostsDone = false;
      })
      .addCase(loadPosts.fulfilled, (state, action) => {
        console.log(action);
        state.mainPosts = state.mainPosts.concat(action.payload);
        state.hasMorePost = action.payload.length === 10;
        state.loadPostsLoading = false;
        state.loadPostsDone = true;
      })
      .addCase(loadPosts.rejected, (state, action) => {
        console.log(action);
        state.loadPostsLoading = false;
        state.loadPostsError = action.error;
      })

      .addDefaultCase((state) => state),
});


export default postSlice;

 

 

back/routes/posts.js

const express = require("express");
const { Op } = require("sequelize");

const { Post, Image, User, Comment } = require("../models");

const router = express.Router();

router.get("/", async (req, res, next) => {
  // GET /posts
  try {
    const where = {};
    if (parseInt(req.query.lastId, 10)) {
      // 초기 로딩이 아닐 때
      where.id = { [Op.lt]: parseInt(req.query.lastId, 10) };
    } // 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
    const posts = await Post.findAll({
      where,
      limit: 10,
      order: [
        ["createdAt", "DESC"],
        [Comment, "createdAt", "DESC"],
      ],
      include: [
        {
          model: User,
          attributes: ["id", "nickname"],
        },
        {
          model: Image,
        },
        {
          model: Comment,
          include: [
            {
              model: User,
              attributes: ["id", "nickname"],
            },
          ],
        },
        {
          model: User, // 좋아요 누른 사람
          as: "Likers",
          attributes: ["id"],
        },
        {
          model: Post,
          as: "Retweet",
          include: [
            {
              model: User,
              attributes: ["id", "nickname"],
            },
            {
              model: Image,
            },
          ],
        },
      ],
    });

    res.status(200).json(posts);
  } catch (error) {
    console.error(error);
    next(error);
  }
});

module.exports = router;

dispatch(loadposts())와 관련된 코드들입니다

 

 

i1004gy님의 프로필 이미지

작성한 질문수

질문하기