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

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

i1004gy님의 프로필 이미지
i1004gy

작성한 질문수

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

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

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

해결된 질문

작성

·

337

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
질문자

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

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

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

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

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

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

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

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

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

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를 밖으로 빼라는 말씀이신가요?

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

네 맞습니다.

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

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

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

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

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

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

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

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

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

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

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

 

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

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

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

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

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

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

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

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

오류가 일어나는 위치는

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

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

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

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님의 프로필 이미지
i1004gy

작성한 질문수

질문하기