Next.js 서버사이드렌더링
버전이 다르기 때문에 소스가 강좌와 다를 수 있다.
버전
next: 13.0.4
antd: 5.0.1
소스 : https://github.dev/braverokmc79/node-bird-sns
제로초 소스 : https://github.com/ZeroCho/react-nodebird
69. getStaticPaths
강의 :
리액트
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)
// if (router.isFallback) {
// return <div>로딩중...</div>;
// }
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} />}
{singlePost == null && '등록된 게시글이 없습니다.'}
</AppLayout>
);
};
//getStaticProps 는 다이나믹 패스에서 사용하는데,
//다이나믹이니깐 어떤것을 만들어 줘야할지 모른다 따라서,
//다음과 해당 id 만 정적인 html 만들어 준다
// export async function getStaticPaths() {
// return {
// paths: [
// { params: { id: '16' } },
// { params: { id: '17' } },
// { params: { id: '18' } },
// ],
// fallback: true,
// };
// }
//getServerSideProps
// export const getStaticProps = wrapper.getStaticProps(async (context) => {
// const cookie = context.req ? context.req.headers.cookie : '';
// console.log("context : ::: ", context);
// axios.defaults.headers.Cookie = '';
// if (context.req && cookie) {
// axios.defaults.headers.Cookie = cookie;
// }
// context.store.dispatch({
// type: LOAD_MY_INFO_REQUEST,
// });
// context.store.dispatch({
// type: LOAD_POST_REQUEST,
// data: context.params.id,
// });
// context.store.dispatch(END);
// await context.store.sagaTask.toPromise();
// });
//새로 고침시 유지
export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => {
console.log(" reqreqreqreq ", req);
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;
70. swr 사용해보기
=> React(Nextjs) 프로젝트는 ★ Redux를 Saga 대체 SWR 사용하기
swr 라이브러리 설치후
npm i swr
다음과 같이 사용하면 된다.
~
const fetcher = (url) => axios.get(url, { withCredentials: true }).then((result) => result.data);
const backUrl = 'http://localhost:3065';
const [followersLimit, setFollowersLimit] = useState(3);
const [followingsLimit, setFollowingsLimit] = useState(3);
const { data: followingsData, error: followingError } = useSWR(`${backUrl}/user/followings?limit=${followingsLimit}`, fetcher);
const { data: followersData, error: followerError } = useSWR(`${backUrl}/user/followers?limit=${followersLimit}`, fetcher);
~
백엔드
routes/user.js
~
//팔로워 불러워기
router.get('/followers', isLoggedIn, async (req, res, next) => { //get /user/followers
console.log(" 팔로워 불러워기 followers : ");
try {
//패스포트에 로그인한 user id 값 : req.user.id
const user = await User.findOne({ where: { id: req.user.id } });
//3개씩 불러오기
const followers = await user.getFollowers({
limit: parseInt(req.query.limit, 10)
});
res.status(200).json(followers)
} catch (error) {
console.error(error);
next(error);
}
});
// 팔로잉 불러워기 시퀄라이즈에서 다음과 같이 Followings 처리를 해서 getFollowings 적용 됨
router.get('/followings', isLoggedIn, async (req, res, next) => { //get /user/followings
try {
const user = await User.findOne({ where: { id: req.user.id } });
const followings = await user.getFollowings({
limit: parseInt(req.query.limit, 10)
});
res.status(200).json(followings);
} catch (error) {
console.error(error);
next(error);
}
});
~
프론트엔드
pages/profile.js
import React, { useEffect, useState, useCallback } from 'react';
import AppLayout from './../components/AppLayout';
import Head from 'next/head';
import { useSelector } from 'react-redux';
import Router from 'next/router';
import FollowList from './../components/FollowList';
import NicknameEditForm from './../components/NicknameEditForm';
import { LOAD_MY_INFO_REQUEST } from '../reducers/user';
import { END } from 'redux-saga';
import wrapper from '../store/configureStore';
import axios from 'axios';
import useSWR from 'swr';
const fetcher = (url) => axios.get(url, { withCredentials: true }).then((result) => result.data);
const backUrl = 'http://localhost:3065';
const Profile = () => {
const { me } = useSelector((state) => state.user);
const [followersLimit, setFollowersLimit] = useState(3);
const [followingsLimit, setFollowingsLimit] = useState(3);
const { data: followingsData, error: followingError } = useSWR(`${backUrl}/user/followings?limit=${followingsLimit}`, fetcher);
const { data: followersData, error: followerError } = useSWR(`${backUrl}/user/followers?limit=${followersLimit}`, fetcher);
useEffect(() => {
if (!(me && me.id)) {
Router.replace('/');
}
}, [me && me.id])
const loadMoreFollowings = useCallback(() => {
setFollowingsLimit((prev) => prev + 3);
}, []);
const loadMoreFollowers = useCallback(() => {
setFollowersLimit((prev) => prev + 3);
}, []);
if (!me) {
return '내 정보 로딩중...';
}
if (followerError || followingError) {
console.error(followerError || followingError);
return <div>팔로잉/팔로워 로딩 중 에러가 발생합니다.</div>;
}
return (
<>
<Head>
<title>프로필 | NodeBird</title>
</Head>
<AppLayout>
<NicknameEditForm />
<div style={{ marginBottom: 20 }}></div>
{/* <FollowList header="팔로잉" data={me.Followings} />
<FollowList header="팔로워" data={me.Followers} /> */}
<FollowList header="팔로잉" data={followingsData} onClickMore={loadMoreFollowings} loading={!followingsData && !followingError} />
<FollowList header="팔로워" data={followersData} onClickMore={loadMoreFollowers} loading={!followersData && !followerError} />
</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(END);
await store.sagaTask.toPromise();
})
export default Profile;
71. 해시태그 검색하기
백엔드
routes/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) => {
console.log(" /hashtag /노드 : ", decodeURIComponent(req.params.hashtag));
let hashtag = decodeURIComponent(req.params.hashtag);
//다음과 같은 형식으로 파라미터를 받을 경우
// /_next/data/development리액트.json?tag=리액트
if (hashtag.indexOf("tag=") !== -1) {
hashtag = hashtag.split("tag=");
hashtag = hashtag[1];
console.log("문자열 포함 :hashtag => ", hashtag);
}
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: 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;
]
프론트엔드
components/AppLayout.js
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import Link from 'next/link'
import { Menu, Input, Row, Col } from 'antd';
import styled from 'styled-components';
import { useSelector } from 'react-redux';
import Router from 'next/router';
import UserProfile from './UserProfile';
import LoginForm from './LoginForm';
import useInput from '../hooks/useInput';
const SearchInput = styled(Input.Search)`
vertical-align: 'middle' ;
`;
const AppLayout = ({ children }) => {
const [searchInput, onChangeSearchInput] = useInput('');
const { me } = useSelector((state) => state.user);
const onClick = useCallback((e) => {
if (e.key === "item-4") {
if (searchInput) {
// onChangeSearchInput("sdfsdf");
console.log("검색어: ", searchInput);
Router.push(`/hashtag/${searchInput}`);
}
}
}, [searchInput]);
const items = [
{ label: <Link href="/" >노드버드</Link>, key: 'item-1' },
me && { label: <Link href="/profile">프로필</Link>, key: 'item-2' },
!me && { label: <Link href="/signup">회원가입</Link>, key: 'item-3' },
{
label: <SearchInput enterButton value={searchInput} onChange={onChangeSearchInput} />,
key: 'item-4'
}
];
return (
<div>
<Menu onClick={onClick} mode="horizontal" items={items} />
<Row gutter={24} style={{ marginTop: 20 }}>
<Col xs={24} md={6} style={{ marginTop: 20 }}>
Hello.Next
</Col>
</Row>
<Row gutter={24} style={{ marginTop: 20 }}>
<Col xs={24} md={6} style={{ marginTop: 20 }}>
{me ? <UserProfile /> : <LoginForm />}
</Col>
<Col xs={24} md={16} style={{ marginTop: 20 }} >
{children}
</Col>
</Row>
<Row gutter={24} style={{ marginTop: 20 }}>
<Col xs={24} md={24} style={{ marginTop: 20 }}>
<a href='https://macaronics.net' target="_blank" rel="noreferrer noopener" >
Made by macaronics
</a>
</Col>
</Row>
</div>
);
};
AppLayout.prototype = {
children: PropTypes.node.isRequired
}
export default AppLayout;
72. moment와 next 빌드하기
강의 :
day.js 사용 방법 - JavaScript 날짜 라이브러리
프론트엔드
components/PostCard.js
~
import moment from 'moment';
~
moment.locale("ko");
const PostCard = ({ post }) => {
~
{post.RetweetId && post.Retweet ? (
<Card
cover={post.Retweet.Images[0] && <PostImages images={post.Retweet.Images} />}
style={{ background: '#eee' }}
>
<div style={{ float: 'right' }}>
{moment(post.createdAt).format('YYYY.MM.DD')}
</div>
<Card.Meta
~
빌드처리
eslint 생략
.eslintignore 파일 생성후
*
빌드
$npm run build
=>
Route (pages) Size First Load JS ┌ λ / 1.52 kB 1.28 MB ├ /_app 0 B 1.04 MB ├ ○ /404 1.52 kB 1.04 MB ├ ● /about (2917 ms) 1.92 kB 1.18 MB ├ λ /hashtag/[tag] 568 B 1.27 MB ├ λ /post/[id] 610 B 1.27 MB ├ λ /profile 6.67 kB 1.23 MB ├ λ /signup 29.3 kB 1.21 MB └ λ /user/[id] 804 B 1.27 MB + First Load JS shared by all 1.04 MB ├ chunks/framework-3b5a00d5d7e8d93b.js 45.4 kB ├ chunks/main-abafce5311b78c60.js 31.2 kB ├ chunks/pages/_app-48dc86756f8fa920.js 961 kB ├ chunks/webpack-2c683dfba3390d25.js 1.89 kB └ css/0e8b7820bc287a84.css 194 B λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps) ○ (Static) automatically rendered as static HTML (uses no initial props) ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
chunks/pages/_app-48dc86756f8fa920.js
=>무엇을 의미하는지 모른다.
다음 라이브러리 설치
$ npm i @next/bundle-analyzer
73. 커스텀 웹팩과 bundle-analyzer
강의 :
next.config.js 설정
$ npm i @next/bundle-analyzer
** production 설정
package.json
"scripts": {
"dev": "next dev -p 3060",
"build": "ANALYZEER=true NODE_ENV=production next build "
},
=> 리눅스 와 맥에서만 적용된다.
따라서, cross-env 설치
$ npm i cross-env
=>
"scripts": {
"dev": "next dev -p 3060",
"build": "cross-env ANALYZE=true NODE_ENV=production next build "
},
next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
//styledComponents: true,
compiler: {
// Enables the styled-components SWC transform
styledComponents: true
},
compress: true,
webpack(config, { webpack }) {
const prod = process.env.NODE_ENV === 'production';
const newConfig = {
...config,
mode: prod ? 'production' : 'development',
plugins: [
...config.plugins,
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /^\.\/ko$/),
],
};
if (prod) {
newConfig.devtool = 'hidden-source-map';
}
return newConfig;
},
}
module.exports = withBundleAnalyzer(nextConfig);

Route (pages) Size First Load JS ┌ λ / 1.52 kB 352 kB ├ /_app 0 B 115 kB ├ ○ /404 1.52 kB 117 kB ├ ● /about (2793 ms) 1.92 kB 261 kB ├ λ /hashtag/[tag] 569 B 351 kB ├ λ /post/[id] 610 B 351 kB ├ λ /profile 6.67 kB 303 kB ├ λ /signup 29.3 kB 289 kB └ λ /user/[id] 805 B 351 kB + First Load JS shared by all 116 kB ├ chunks/framework-3b5a00d5d7e8d93b.js 45.4 kB ├ chunks/main-abafce5311b78c60.js 31.2 kB ├ chunks/pages/_app-08f11cd4ffdb460a.js 37 kB ├ chunks/webpack-579f3e5ed9d92545.js 1.89 kB └ css/0e8b7820bc287a84.css 194 B λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps) ○ (Static) automatically rendered as static HTML (uses no initial props) ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)














댓글 ( 4)
댓글 남기기