Next.js 서버사이드렌더링
버전이 다르기 때문에 소스가 강좌와 다를 수 있다.
버전
next: 13.0.4
antd: 5.0.1
소스 : https://github.dev/braverokmc79/node-bird-sns
제로초 소스 : https://github.com/ZeroCho/react-nodebird
63. 서버사이드렌더링 준비하기
강의 :
리듀서 설정 오류 변 경
reducer/index.js
import { HYDRATE } from 'next-redux-wrapper'; import { combineReducers } from 'redux'; import user from './user'; import post from './post'; //(이전상태,액션) => 다음 상태 const rootReducer = (state, action) => { switch (action.type) { case HYDRATE: { console.log('HYDRATE', action); return action.payload; } default: { const combinedReducer = combineReducers({ user, post }); return combinedReducer(state, action); } } } export default rootReducer;
참조 :
서버 사이드 랜더링 적용
적용 오류시 다음 참조 :
pages/index.js
import React, { useEffect, useCallback } from 'react'; import AppLayout from './../components/AppLayout'; import { useSelector, useDispatch } from 'react-redux'; import PostCard from './../components/PostCard'; import PostForm from './../components/PostForm'; import { LOAD_MY_INFO_REQUEST } from './../reducers/user'; import { LOAD_POSTS_REQUEST } from './../reducers/post'; import { END } from 'redux-saga'; import wrapper from '../store/configureStore'; const Home = () => { const dispatch = useDispatch(); const { me } = useSelector((state) => state.user); const { mainPosts, hasMorePosts, loadPostsLoading, reTweetError, reTweetDone } = useSelector((state) => state.post); useEffect(() => { if (reTweetError) { return alert(reTweetError); } }, [reTweetError]); useEffect(() => { if (reTweetDone) { alert("리트윗 되었습니다."); } }, [reTweetDone]); useEffect(() => { function onScroll() { if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) { // console.log("hasMorePosts && !loadPostsLoading : ", hasMorePosts, loadPostsLoading); if (hasMorePosts && !loadPostsLoading) { const lastId = mainPosts[mainPosts.length - 1]?.id; // console.log(" lastId : ", lastId); dispatch({ type: LOAD_POSTS_REQUEST, lastId }) } } } window.addEventListener('scroll', onScroll); //항상 반환처리시 이벤트를 제거해야지 메모리상에 낭비를 줄일 수 있다. return () => { window.removeEventListener('scroll', onScroll); } }, [hasMorePosts, loadPostsLoading, mainPosts]); return ( <AppLayout> {me && <PostForm />} {mainPosts && mainPosts.map((post) => <PostCard key={post.id} post={post} />)} </AppLayout> ); }; // 서버사이드 렌더링 : 프론트 서버가 직접 요청하기 때문에 withCredentials문제 다시 발생하므로 브러우저 대신 cookie를 보내줘야함. //home 실행되기전에 가장 먼저 실행 된다. //초기 화면 랜더될때에는 리덕스에 데이터가 채워진체로 실행 export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc ) => { console.log('getServerSideProps start'); //console.log(context.req.headers); store.dispatch({ type: LOAD_MY_INFO_REQUEST, }); store.dispatch({ type: LOAD_POSTS_REQUEST }); //다음 코드는 nextjs 문서 store.dispatch(END); console.log('getServerSideProps end'); await store.sagaTask.toPromise(); }) export default Home;
콘솔로그가 웹 브라우저 창이 아닌 서버 터미널에 찍힌다.
64. SSR시 쿠키 공유하기
강의:
로그인이 풀리는 현상 은 쿠키값이 백엔드 서버에 전달되지 않아서 ,따라서 쿠키 전달.
그리고 공유 시 주의 사항
const cookie = req ? req.headers.cookie : ''; axios.defaults.headers.Cookie = ''; //*** 쿠키가 공유될수 있으므로 쿠키 초기화 필수 if (req && cookie) { //서버일때와 쿠키가 존재할때만 실행 axios.defaults.headers.Cookie = cookie; }
프론트엔드
pages/index.js
import React, { useEffect, useCallback } from 'react'; import AppLayout from './../components/AppLayout'; import { useSelector, useDispatch } from 'react-redux'; import PostCard from './../components/PostCard'; import PostForm from './../components/PostForm'; import { LOAD_MY_INFO_REQUEST } from './../reducers/user'; import { LOAD_POSTS_REQUEST } from './../reducers/post'; import { END } from 'redux-saga'; import wrapper from '../store/configureStore'; import axios from 'axios'; const Home = () => { const dispatch = useDispatch(); const { me } = useSelector((state) => state.user); const { mainPosts, hasMorePosts, loadPostsLoading, reTweetError, reTweetDone } = useSelector((state) => state.post); useEffect(() => { if (reTweetError) { return alert(reTweetError); } }, [reTweetError]); useEffect(() => { if (reTweetDone) { alert("리트윗 되었습니다."); } }, [reTweetDone]); useEffect(() => { function onScroll() { if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) { // console.log("hasMorePosts && !loadPostsLoading : ", hasMorePosts, loadPostsLoading); if (hasMorePosts && !loadPostsLoading) { const lastId = mainPosts[mainPosts.length - 1]?.id; // console.log(" lastId : ", lastId); dispatch({ type: LOAD_POSTS_REQUEST, lastId }) } } } window.addEventListener('scroll', onScroll); //항상 반환처리시 이벤트를 제거해야지 메모리상에 낭비를 줄일 수 있다. return () => { window.removeEventListener('scroll', onScroll); } }, [hasMorePosts, loadPostsLoading, mainPosts]); return ( <AppLayout> {me && <PostForm />} {mainPosts && mainPosts.map((post) => <PostCard key={post.id} post={post} />)} </AppLayout> ); }; // 서버사이드 렌더링 : 프론트 서버가 직접 요청하기 때문에 withCredentials문제 다시 발생하므로 브러우저 대신 cookie를 보내줘야함. //home 실행되기전에 가장 먼저 실행 된다. //초기 화면 랜더될때에는 리덕스에 데이터가 채워진체로 실행 //다음 코드는 프론트 서버에서 실행 //도메인이 다르면 쿠키전달 안된다. export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => { console.log('getServerSideProps start'); //서버에 쿠키가 전달이 안된다. 따라서 새로고침시 로그인 풀리는 현상 //따라서 다음과 같은 코드로 서버에 쿠키값을 보내는 처리를 한다. console.log(" store :", store); const cookie = req ? req.headers.cookie : ''; axios.defaults.headers.Cookie = ''; //*** 쿠키가 공유될수 있으므로 쿠키 초기화 필수 if (req && cookie) { //서버일때와 쿠키가 존재할때만 실행 axios.defaults.headers.Cookie = cookie; } console.log(" req cookie :", cookie); store.dispatch({ type: LOAD_MY_INFO_REQUEST, }); store.dispatch({ type: LOAD_POSTS_REQUEST }); //다음 코드는 nextjs 문서 store.dispatch(END); console.log('getServerSideProps end'); await store.sagaTask.toPromise(); }) export default Home;
백엔드
const express = require('express'); const { User, Post } = require('../models'); const bcrypt = require('bcrypt'); const passport = require('passport'); const router = express.Router(); const { isLoggedIn, isNotLoggedIn } = require('./middlewares'); //브라우저에서 새로고침 할때마다 요청처리 된다. router.get('/', async (req, res, next) => { console.log("브라우저에서 새로고침 할때마다 요청처리 :", req.headers); try { if (req.user) { const fullUserWithoutPassword = await User.findOne({ where: { id: req.user.id }, attributes: { exclude: ['password'] }, include: [{ model: Post, attributes: ['id'], }, { model: User, as: "Followers", attributes: ['id'], }, { model: User, as: "Followings", attributes: ['id'], } ] }) res.status(200).json(fullUserWithoutPassword); } else { res.status(200).json(null); } } catch (error) { console.error("/ 쿠키 정보 가져오기 에러 : ", error); next(error); } }); ~
65. getStaticProps 사용해보기
강의 :
SSR 과 getStaticProps 차이는 언제 써도 변경하지 않는다면 getStaticProps 을 사용
접속한 환경에 따라 변화 한다면 SSR 사용 , getStaticProps 까다롭다. 대부분 SSR 사용
pages/about.js
import React from 'react'; import { useSelector } from 'react-redux'; import Head from 'next/head'; import { END } from 'redux-saga'; import { Avatar, Card } from 'antd'; import AppLayout from '../components/AppLayout'; import wrapper from '../store/configureStore'; import { LOAD_USER_INFO_REQUEST } from '../reducers/user'; const Profile = () => { const { userInfo } = useSelector((state) => state.user); return ( <AppLayout> <Head> <title>ZeroCho | NodeBird</title> </Head> {userInfo ? ( <Card actions={[ <div key="twit"> 짹짹 <br /> {userInfo.Posts} </div>, <div key="following"> 팔로잉 <br /> {userInfo.Followings} </div>, <div key="follower"> 팔로워 <br /> {userInfo.Followers} </div>, ]} > <Card.Meta avatar={<Avatar>{userInfo.nickname[0]}</Avatar>} title={userInfo.nickname} description="노드버드 매니아" /> </Card> ) : null} </AppLayout> ); }; export const getStaticProps = wrapper.getStaticProps((store) => async (req, res, ...etc) => { console.log('getStaticProps : static 으로 특정한 사용자 정보 가져오기 1 번 유저 : '); store.dispatch({ type: LOAD_USER_INFO_REQUEST, data: 1, }); store.dispatch(END); await store.sagaTask.toPromise(); }); export default Profile;
백엔드
routes/user.js
~ //특정 유저 정보 가져오기 router.get('/:userId', async (req, res, next) => { console.log(" 유저 정보 가져오기 : ", req.params.userId); try { const fullUserWithoutPassword = await User.findOne({ where: { id: req.params.userId }, attributes: { exclude: ['password'] }, include: [{ model: Post, attributes: ['id'], }, { model: User, as: "Followers", attributes: ['id'], }, { model: User, as: "Followings", attributes: ['id'], } ] }) if (fullUserWithoutPassword) { const data = fullUserWithoutPassword.toJSON(); //개인정보 침해 예방을 위해 서버에서 데이터길이만 변경해서 보내 준다. data.Posts = data.Posts.length; data.Followings = data.Followings.length; data.Followers = data.Followers.length; res.status(200).json(data); } else { res.status(404).json("존재하지 않는 사용자입니다."); } } catch (error) { console.error("/ 쿠키 정보 가져오기 에러 : ", error); next(error); } });
profile.js , signup.js ServerSideProps 적용
~ //새로 고침시 유지 export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => { const cookie = req ? req.headers.cookie : ''; axios.defaults.headers.Cookie = ''; if (req && cookie) { axios.defaults.headers.Cookie = cookie; } store.dispatch({ type: LOAD_MY_INFO_REQUEST, }); store.dispatch(END); await store.sagaTask.toPromise(); })
66. 다이나믹 라우팅
강의 :
백엔드
routes/post.js
~ //GET /post 한개 정보 가져오기 router.get('/:postId', async (req, res, next) => { console.log(" 한개의. 정보 가져오기 : ", req.params.postId); try { const posts = await Post.findOne({ where: { id: req.params.postId }, 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 } ] } ] }); console.log(" 한개의. 정보 posts : ", posts); res.status(200).json(posts); } catch (error) { console.error("posts error : ", error); next(error); } }); ~
프론트엔드
pages/post/[id].js
//post/[id].js import { useSelector } from 'react-redux'; import Head from 'next/head'; import { useRouter } from 'next/router'; import AppLayout from '../../components/AppLayout'; import PostCard from '../../components/PostCard'; import { LOAD_POST_REQUEST } from './../../reducers/post'; import { LOAD_MY_INFO_REQUEST } from './../../reducers/user'; import { END } from 'redux-saga'; import wrapper from '../../store/configureStore'; import axios from 'axios'; const Post = () => { const router = useRouter(); const { id } = router.query; const { singlePost } = useSelector((state) => state.post) return ( <AppLayout> {singlePost && <Head> <title> {singlePost.User.nickname} 님의 글 </title> <meta name="description" content={singlePost.content} /> <meta property="og:title" content={`${singlePost.User.nickname}님의 게시글`} /> <meta property="og:description" content={singlePost.content} /> <meta property="og:image" content={singlePost.Images[0] ? singlePost.Images[0].src : 'https://nodebird.com/favicon.ico'} /> <meta property="og:url" content={`https://nodebird.com/post/${id}`} /> </Head> } {singlePost && <PostCard key={id && id} post={singlePost} />} </AppLayout> ); }; //새로 고침시 유지 export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => { const cookie = req ? req.headers.cookie : ''; axios.defaults.headers.Cookie = ''; if (req && cookie) { axios.defaults.headers.Cookie = cookie; } store.dispatch({ type: LOAD_MY_INFO_REQUEST, }); store.dispatch({ type: LOAD_POST_REQUEST, postId: req.url.replace('/post/', '') }); store.dispatch(END); await store.sagaTask.toPromise(); }) export default Post;
reducers/post.js
~ //한개의 POST 정보 불러오기 case LOAD_POST_REQUEST: draft.loadPostLoading = true; draft.loadPostDone = false; draft.loadPostError = null; break; case LOAD_POST_SUCCESS: draft.loadPostLoading = false; draft.loadPostDone = true; draft.singlePost = action.data; break; case LOAD_POST_FAILURE: draft.loadPostLoading = false; draft.loadPostError = action.error; break; ~
saga/post.js
~ //한개의 POST 정보 불러오기 function loadPostAPI(postId) { return axios.get(`/post/${postId}`); } function* loadPost(action) { try { const result = yield call(loadPostAPI, action.postId); yield put({ type: LOAD_POST_SUCCESS, data: result.data }); } catch (err) { console.log(err); yield put({ type: LOAD_POST_FAILURE, error: err.response.data }); } } function* watchLoadPost() { yield takeLatest(LOAD_POST_REQUEST, loadPost); } ~
67. CSS 서버사이드렌더링
강의 :
1 ) 라이브러리 설치
$ npm i babel-plugin-styled-components
2 ) .babelrc 추가
{ "presets": ["next/babel"], "plugins": [["babel-plugin-styled-components", { "ssr": true, "displayName" :true } ]] }
3) pages/_document.js
import React from 'react'; import Document, { Html, Head, Main, NextScript } from 'next/document'; import { ServerStyleSheet } from 'styled-components'; export default class MyDocument extends Document { static async getInitialProps(ctx) { const sheet = new ServerStyleSheet(); const originalRenderPage = ctx.renderPage; try { ctx.renderPage = () => originalRenderPage({ enhanceApp: App => props => sheet.collectStyles(<App {...props} />), }); const initialProps = await Document.getInitialProps(ctx); return { ...initialProps, styles: ( <> {initialProps.styles} {sheet.getStyleElement()} </> ), }; } catch (error) { console.error(error); } finally { sheet.seal(); } } render() { return ( <Html> <Head /> <body> <Main /> <NextScript /> </body> </Html> ); } }
Next.js의 _document 파일은 무엇입니까?
업데이트:2022년 7월 10일
https://www.webdevtutor.net/blog/what-is-the-document-file-in-nextjs
Next.js에서 _document 파일의 중요성
이 _document파일은 Next.js 애플리케이션에서 페이지의 초기화 프로세스를 전체적으로 제어할 수 있게 해주는 몇 가지 중요한 파일 중 하나입니다.
Next.js 애플리케이션을 전체적으로 제어할 수 있는 다른 파일로는 , _app.js및 가 있습니다._document.jsnext.config.jspackage.json
Next.js의 _document 파일은 무엇입니까?
_document 파일은 페이지 초기화 중에 호출되며 기본 문서 페이지를 재정의하는 데 사용됩니다. _document 파일 내에서 HTML 및 Body 태그를 업데이트하여 기본 구현을 재정의할 수 있습니다.
_document.js파일은 Next.js 애플리케이션 폴더의 루트에 있는 폴더 pages에 있습니다 pages/_document.js.
파일은 기본적으로 포함되어 있지 않으므로 파일이 보이지 않으면 에서 빈 파일을 만드 pages/_document.js십시오.
_document 파일의 사용 사례
_documentNext.js 웹 개발 프로세스 중에 파일 을 사용자 지정하려는 두 가지 일반적인 사용 사례를 살펴보겠습니다 .
_document이 게시물에서는 파일 을 사용자 지정하기 위한 다음 두 가지 일반적인 사용 사례에 대해 알아봅니다 .
HTML 태그에 사용자 정의 언어 추가
HTMLNext.js의 태그에 사용자 정의 언어를 추가하려는 경우 _document파일이 이를 위한 완벽한 장소입니다.
English다음은 애플리케이션에서 생성된 모든 HTML 파일에 기본 언어를 추가하는 코드 예제입니다 .
import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { return ( <Html lang="en"> <Head /> <body> <Main /> <NextScript /> </body> </Html> ); }
Body 태그에 사용자 정의 CSS 클래스 추가
body파일 을 업데이트하면 Next.js 애플리케이션 의 태그에 맞춤 CSS 클래스를 쉽게 추가 할 수 _document있습니다.
이 코드 예제에서 bodyNext.js에 의해 생성된 모든 HTML 페이지의 전체 요소는 검정색 배경을 갖습니다.
import { Html, Head, Main, NextScript } from "next/document"; export default function Document() { return ( <Html> <Head /> <body className="bg-black"> <Main /> <NextScript /> </body> </Html> ); }
추천도서
Next.js의 _document 파일과 _app 파일 비교
_document 파일과 _app 파일은 Next.js에서 중요한 두 파일입니다. 이 블로그 게시물은 _document 파일과 _app 파일을 비교하고 대조합니다.
Next.js의 _document 파일과 _app 파일 비교
Next.js 애플리케이션의 _document 파일 이해 요약
이 블로그 게시물에서는 HTML 태그를 사용자 정의하는 방법과 사용자 정의 클래스 이름을 body 태그에 추가하는 방법을 배웠습니다 .
Next.JS 애플리케이션에 추가 사용자 지정을 추가 하려면 Next.js의 _app 파일에 대한 사용 사례를 확인하세요 .
✍️ 새 블로그 게시물
???? NextJS 마스터리
아래 기사를 선택하여 NextJS 숙달의 길을 계속 가십시오 !
Next.js 앱을 위한 5가지 SEO 팁
SEO가 중요합니다. Next.js 앱에서 제대로 구현되었나요? SEO를 개선하려면 다음 5가지 팁을 따르세요!
Next.js의 페이지에 메타 태그 추가
Next.js에서 SEO를 하고 계십니까? 이 가이드를 따라 메타 태그를 사용하세요!
Next.js의 링크 구성 요소에서 onclick 이벤트 처리
Next.js에서 onclick 이벤트를 처리하는 데 문제가 있습니까? 이 게시물은 링크 구성 요소로 이러한 이벤트를 처리하는 방법을 보여줍니다.
Next.js의 링크 구성 요소에서 onclick 이벤트 처리
68. 사용 게시글, 해시태그 게시글
강의 :
백엔드
back/hashtag.js
const express = require('express'); const { Op } = require('sequelize'); const { Post, Hashtag, Image, User, Comment } = require('../models'); const router = express.Router(); //GET /hashtag /노드 router.get('/:hashtag', async (req, res, next) => { try { const where = {}; if (parseInt(req.query.lastId, 10)) { //초기 로딩이 아닐때 //다음 코드 내용은 id 가 lastId 보다 작은 것 => id < lastId // Op 의미는 연산자 의미 lt 는 < where.id = { [Op.lt]: parseInt(req.query.lastId, 10) } }; const posts = await Post.findAll({ where, limit: 10, order: [['createdAt', 'DESC'],], include: [ { model: Hashtag, where: { name: decodeURIComponent(req.params.hashtag) } }, { 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("posts error : ", error); next(error); } }); module.exports = router;
back/user.js
const express = require('express'); const { User, Post, Comment, Image } = require('../models'); const { Op } = require('sequelize'); const bcrypt = require('bcrypt'); const passport = require('passport'); const router = express.Router(); const { isLoggedIn, isNotLoggedIn } = require('./middlewares'); ~ //특정 사용자에 대한 게시글 목록 //GET /posts router.get('/:userId/posts', async (req, res, next) => { try { console.log(" 특정 사용자에 대한 게시글 목록 :", req.query.lastId); const where = { UserId: req.params.userId }; if (parseInt(req.query.lastId, 10)) { //초기 로딩이 아닐때 //다음 코드 내용은 id 가 lastId 보다 작은 것 => id < lastId // Op 의미는 연산자 의미 lt 는 < where.id = { [Op.lt]: parseInt(req.query.lastId, 10) } }; const posts = await Post.findAll({ where, limit: 10, order: [ ['createdAt', 'DESC'], [Comment, 'createdAt', 'DESC'] ], // offsset: parseInt(req.params.limit), //21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1 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("posts error : ", error); next(error); } });
프론트엔드
pages/[tag].js
// hashtag/[tag].js import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useRouter } from 'next/router'; import { END } from 'redux-saga'; import axios from 'axios'; import { LOAD_HASHTAG_POSTS_REQUEST } from '../../reducers/post'; import PostCard from '../../components/PostCard'; import wrapper from '../../store/configureStore'; import { LOAD_MY_INFO_REQUEST } from '../../reducers/user'; import AppLayout from '../../components/AppLayout'; const Hashtag = () => { const dispatch = useDispatch(); const router = useRouter(); const { tag } = router.query; const { mainPosts, hasMorePosts, loadPostsLoading } = useSelector((state) => state.post); useEffect(() => { const onScroll = () => { if (window.pageYOffset + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) { if (hasMorePosts && !loadPostsLoading) { dispatch({ type: LOAD_HASHTAG_POSTS_REQUEST, lastId: mainPosts[mainPosts.length - 1] && mainPosts[mainPosts.length - 1].id, data: tag, }); } } }; window.addEventListener('scroll', onScroll); return () => { window.removeEventListener('scroll', onScroll); }; }, [mainPosts.length, hasMorePosts, tag, loadPostsLoading]); return ( <AppLayout> {mainPosts.map((c) => ( <PostCard key={c.id} post={c} /> ))} </AppLayout> ); }; export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => { const cookie = req ? req.headers.cookie : ''; axios.defaults.headers.Cookie = ''; if (req && cookie) { axios.defaults.headers.Cookie = cookie; } store.dispatch({ type: LOAD_MY_INFO_REQUEST, }); store.dispatch({ type: LOAD_HASHTAG_POSTS_REQUEST, data: req.url.replace('/hashtag/', '') }); store.dispatch(END); await store.sagaTask.toPromise(); }); export default Hashtag;
pages/[id].js
import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Avatar, Card } from 'antd'; import { END } from 'redux-saga'; import Head from 'next/head'; import { useRouter } from 'next/router'; import axios from 'axios'; import { LOAD_USER_POSTS_REQUEST } from '../../reducers/post'; //특정 사용자의 글 import { LOAD_MY_INFO_REQUEST, LOAD_USER_INFO_REQUEST } from '../../reducers/user'; import PostCard from '../../components/PostCard'; import wrapper from '../../store/configureStore'; import AppLayout from '../../components/AppLayout'; //특정 사용자에 대한 게시글 목록 const User = () => { const dispatch = useDispatch(); const router = useRouter(); const { id } = router.query; const { mainPosts, hasMorePosts, loadPostsLoading } = useSelector((state) => state.post); const { userInfo, me } = useSelector((state) => state.user); useEffect(() => { const onScroll = () => { if (window.pageYOffset + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) { if (hasMorePosts && !loadPostsLoading) { dispatch({ type: LOAD_USER_POSTS_REQUEST, lastId: mainPosts[mainPosts.length - 1] && mainPosts[mainPosts.length - 1].id, data: id, }); } } }; window.addEventListener('scroll', onScroll); return () => { window.removeEventListener('scroll', onScroll); }; }, [mainPosts.length, hasMorePosts, id, loadPostsLoading]); return ( <AppLayout> {userInfo && ( <div>{userInfo.nickname}</div> // <Head> // <title> // {userInfo.nickname} // 님의 글 // </title> // <meta name="description" content={`${userInfo.nickname}님의 게시글`} /> // <meta property="og:title" content={`${userInfo.nickname}님의 게시글`} /> // <meta property="og:description" content={`${userInfo.nickname}님의 게시글`} /> // <meta property="og:image" content="https://nodebird.com/favicon.ico" /> // <meta property="og:url" content={`https://nodebird.com/user/${id}`} /> // </Head> )} {userInfo && (userInfo.id !== me?.id) ? ( <Card style={{ marginBottom: 20 }} actions={[ <div key="twit"> 짹짹 <br /> {userInfo.Posts} </div>, <div key="following"> 팔로잉 <br /> {userInfo.Followings} </div>, <div key="follower"> 팔로워 <br /> {userInfo.Followers} </div>, ]} > <Card.Meta avatar={<Avatar>{userInfo.nickname[0]}</Avatar>} title={userInfo.nickname} /> </Card> ) : null} {mainPosts.map((c) => ( <PostCard key={c.id} post={c} /> ))} </AppLayout> ); }; export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => { const cookie = req ? req.headers.cookie : ''; axios.defaults.headers.Cookie = ''; if (req && cookie) { axios.defaults.headers.Cookie = cookie; } store.dispatch({ type: LOAD_USER_POSTS_REQUEST, data: req.url.replace('/user/', ''), }); store.dispatch({ type: LOAD_MY_INFO_REQUEST, }); store.dispatch({ type: LOAD_USER_INFO_REQUEST, data: req.url.replace('/user/', ''), }); store.dispatch(END); await store.sagaTask.toPromise(); }) export default User;
reducers/post.js
//특정 사용자 게시글 export const LOAD_USER_POSTS_REQUEST = 'LOAD_USER_POSTS_REQUEST'; export const LOAD_USER_POSTS_SUCCESS = 'LOAD_USER_POSTS_SUCCESS'; export const LOAD_USER_POSTS_FAILURE = 'LOAD_USER_POSTS_FAILURE'; //특정 해쉬 글 export const LOAD_HASHTAG_POSTS_REQUEST = 'LOAD_HASHTAG_POSTS_REQUEST'; export const LOAD_HASHTAG_POSTS_SUCCESS = 'LOAD_HASHTAG_POSTS_SUCCESS'; export const LOAD_HASHTAG_POSTS_FAILURE = 'LOAD_HASHTAG_POSTS_FAILURE'; ~ //무한 스크롤 case LOAD_USER_POSTS_REQUEST: case LOAD_HASHTAG_POSTS_REQUEST: case LOAD_POSTS_REQUEST: draft.loadPostsLoading = true; draft.loadPostsDone = false; draft.loadPostsError = null; break; case LOAD_USER_POSTS_SUCCESS: case LOAD_HASHTAG_POSTS_SUCCESS: case LOAD_POSTS_SUCCESS: draft.loadPostsLoading = false; draft.loadPostsDone = true; draft.mainPosts = draft.mainPosts.concat(action.data); // console.log("무한 스크롤 : ", draft.mainPosts); //draft.mainPosts = action.data.concat(draft.mainPosts); draft.hasMorePosts = action.data.length === 10; break; case LOAD_USER_POSTS_FAILURE: case LOAD_HASHTAG_POSTS_FAILURE: case LOAD_POSTS_FAILURE: draft.loadPostsLoading = false; draft.loadPostsError = action.error; break; ~
saga/post.js
~ import { ~ LOAD_USER_POSTS_REQUEST, LOAD_USER_POSTS_SUCCESS, LOAD_USER_POSTS_FAILURE, LOAD_HASHTAG_POSTS_REQUEST, LOAD_HASHTAG_POSTS_SUCCESS, LOAD_HASHTAG_POSTS_FAILURE } from '../reducers/post' //해시 태그에 대한 게시글 목록 function loadHashtagPostsAPI(data, lastId) { return axios.get(`/hashtag/${encodeURIComponent(data)}?lastId=${lastId || 0}`); } function* loadHashtagPosts(action) { try { const result = yield call(loadHashtagPostsAPI, action.data, action.lastId); yield put({ type: LOAD_HASHTAG_POSTS_SUCCESS, data: result.data }); } catch (err) { console.log(err); yield put({ type: LOAD_HASHTAG_POSTS_FAILURE, error: err.response.data }); } } function* watchLoadHashtagPosts() { yield takeLatest(LOAD_HASHTAG_POSTS_REQUEST, loadHashtagPosts); } //특정 사용자에 대한 게시글 function loadUserPostsAPI(data, lastId) { return axios.get(`/user/${data}/posts?lastId=${lastId || 0}`); } function* loadUserPosts(action) { try { const result = yield call(loadUserPostsAPI, action.data, action.lastId); yield put({ type: LOAD_USER_POSTS_SUCCESS, data: result.data }); } catch (err) { console.log(err); yield put({ type: LOAD_USER_POSTS_FAILURE, error: err.response.data }); } } function* watchLoadUserPosts() { yield takeLatest(LOAD_USER_POSTS_REQUEST, loadUserPosts); } export default function* postSaga() { yield all([ ~ fork(watchLoadHashtagPosts), fork(watchLoadUserPosts) ]); }
saga/user.js
~ // 유저정보 가져오기 function loadUserInfoAPI(userId) { //console.log("사가 유저 정보 가져오기 : ", userId); return axios.get(`/user/${userId}`); } ~ //1-3 로그인 처리 function* watchLogIn() { //LOG_IN 실행 될때 까지 기다리겠다. //console.log("2. watchLogIn "); yield takeLatest(LOG_IN_REQUEST, login); }
댓글 ( 4)
댓글 남기기