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)
댓글 남기기