안녕하세요?
지난 시간에 이어 NextJS와 MongoDB를 이용한 유저 로그인 세션 구현하기 4편을 계속 이어 나가도록 하겠습니다.
1편 : https://cpro95.tistory.com/463
2편 : https://cpro95.tistory.com/465
3편 : https://cpro95.tistory.com/478
4편: https://cpro95.tistory.com/493
1편에서는 기본 준비작업을 했었고,
2편에서는 로그인, 로그아웃
3편에서는 유저 정보 업데이트와 패스워드 변경까지 알아보았습니다.
4편에서는 UI 부분을 Chakra-UI로 적용했습니다.
5편에서는 NextJS의 핵심기능인 Serverless Function 인 SSR, SSG, ISR에 대해 실전 예제를 통해 알아 보겠습니다.
참고로 NextJS의 Data Fetching 방법에 대해 원론적인 공부를 하시고 싶으시면 아래 글을 참고 바랍니다.
https://cpro95.tistory.com/492
Add Post 작성
일단 SSG(Server Side Rendering) 이나 SSG (Static Site Generation) 이나, ISR(Incremental Static Regeneration)을 할려면 그 대상이 있어야 겠죠.
먼저 Post를 서버에 업로드하는 컴포넌트를 만들어 보겠습니다.
/pages/index.js
import React from "react";
import Link from "next/link";
import { useCurrentUser } from "../hooks/index";
import { Flex, VStack, Heading, Text, Button } from "@chakra-ui/react";
const IndexPage = () => {
const [user] = useCurrentUser();
return (
<>
<Flex align="center" justify="center">
<VStack>
<Heading m={4}>Welcome!</Heading>
{user ? (
<Text fontSize="2xl">
Hello, {user.name}
</Text>
) : (
<Text fontSize="2xl">
Please, Log In
</Text>
)}
<Link href="/addpost">
<Button size="lg" colorScheme="telegram">Add Post</Button>
</Link>
</VStack>
</Flex>
</>
);
};
export default IndexPage;
/pages/addpost.js
import React, { useState, useEffect } from "react";
import Head from "next/head";
import { useCurrentUser } from "@/hooks/index";
import { Flex, Heading, Input, Button, Text, VStack } from "@chakra-ui/react";
export default function AddPostPage() {
const title = "Add Post";
const [user] = useCurrentUser();
const [msg, setMsg] = useState({ message: "", isError: false });
async function hanldeSubmit(e) {
e.preventDefault();
const body = {
content: e.currentTarget.content.value,
};
if (!e.currentTarget.content.value) return;
e.currentTarget.content.value = "";
const res = await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (res.ok) {
setMsg("Posted!");
setTimeout(() => setMsg(null), 5000);
}
}
if (!user) {
return (
<>
<Head>
<title>{title}</title>
</Head>
<Text fontSize="2xl">Please, Log In</Text>
</>
);
} else {
return (
<>
<Head>
<title>{title}</title>
</Head>
<Flex align="center" justify="center">
<VStack>
<Heading m={4}>Add Post</Heading>
<VStack direction="column" spacing={8}>
{msg.message ? (
<Text align="center" colorSchme={msg.isError ? "red" : "blue"}>
{msg.message}
</Text>
) : null}
<form onSubmit={hanldeSubmit} autoComplete="off">
<Input
size="lg"
name="content"
type="text"
placeholder="Add query"
/>
<Button w="full" mt={4} size="lg" type="submit">
Post
</Button>
</form>
</VStack>
</VStack>
</Flex>
</>
);
}
}
/pages/index.js 파일에 Add Post 링크를 하나 걸었습니다.
그럼 본격적으로 /pages/addpost.js 파일을 분석해 볼까요?
Input 컴포넌트에서 간단한 글을 입력하면 그걸 이용해서 content 로 저장해서 form submit을 이용해 /api/posts 라우팅에 POST 메쏘드로 글을 올리는 로직입니다.
그럼 /api/posts 라우팅을 구현해야겠네요.
/pages/api/posts/index.js
import nc from 'next-connect';
import { all } from '@/middlewares/index';
import { insertPost } from '@/db/index';
const handler = nc();
handler.use(all);
handler.post(async (req, res) => {
if (!req.user) {
return res.status(401).send('unauthenticated');
}
if (!req.body.content) return res.status(400).send('You must write something');
const post = await insertPost(req.db, {
content: req.body.content,
creatorId: req.user._id,
});
return res.json({ post });
});
export default handler;
API 라우팅은 handler.post를 이용했으며,
req.body부분에 content 부분이 우리가 입력했던 Post가 됩니다.
그래서 그걸 insertPost 함수를 이용해 mongoDB에 업로드하는 로직입니다.
그리고 업로드할 때 creatorId를 user._id로 지정해서 누가 업로드했는지 구분하고 있습니다.
그럼, insertPost 함수를 살펴볼까요?
/db/post.js
import { nanoid } from 'nanoid';
export async function insertPost(db, { content, creatorId }) {
return db.collection('posts').insertOne({
_id: nanoid(12),
content,
creatorId,
createdAt: new Date(),
}).then(({ ops }) => ops[0]);
}
insertPost 함수는 간단합니다.
MongoDB 를 불러오고 collection('posts')을 지정한 다음 insertOne mongodb 함수를 이용해 저장합니다.
insertOne 함수는 성공하면 배열(여기서는 ops)을 리턴하는데 그 첫번째 배열값을 다시 프로미스로 돌려주는 로직입니다.
이제 Post 도 서버에 업로드 했으니까 그 Post를 읽어 들이는 로직을 만들어 보겠습니다.
여기서 우리가 오늘 주제로 하는 SSR, SSG, ISR 에 대해 심도 있게 알아볼 시간이 됐습니다.
Static Site Generation
일단 /pages/index.js 에 list-ssg 링크 버튼을 추가합시다.
import React from "react";
import Link from "next/link";
import { useCurrentUser } from "../hooks/index";
import { Flex, VStack, Heading, Text, Button } from "@chakra-ui/react";
const IndexPage = () => {
const [user] = useCurrentUser();
return (
<>
<Flex align="center" justify="center">
<VStack>
<Heading m={4}>Welcome!</Heading>
{user ? (
<Text fontSize="2xl">Hello, {user.name}</Text>
) : (
<Text fontSize="2xl">Please, Log In</Text>
)}
<Link href="/addpost">
<Button size="lg" colorScheme="telegram">
Add Post
</Button>
</Link>
<Link href="/list-ssg">
<Button size="lg" colorScheme="telegram">
List Post - SSG
</Button>
</Link>
</VStack>
</Flex>
</>
);
};
export default IndexPage;
/pages/list-ssg.js 파일을 통해 SSG 방식으로 Post 리스트를 구현하도록 하겠습니다.
/pages/list-ssg.js
import React, { useState, useEffect } from "react";
import Head from "next/head";
import Error from "next/error";
import {
Flex,
VStack,
Heading,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
} from "@chakra-ui/react";
import { MongoClient } from "mongodb";
global.mongo = global.mongo || {};
export default function ListPostsPage({ posts }) {
const title = "List with Static Site Generation";
if (!posts) return <Error statusCode={404} />;
return (
<>
<Head>
<title>{title}</title>
</Head>
<Flex justify="center">
<Flex w="full" p={[0, 4, 6]}>
<VStack w="full" h="full" spacing={5}>
<Heading mb={2}>Total Posts : {posts.length} </Heading>
<Table size="sm" mb={12}>
<Thead>
<Tr>
<Th>Time</Th>
<Th>Contents</Th>
</Tr>
</Thead>
<Tbody>
{posts.map((post) => (
<Tr key={post.createdAt.toString()}>
<Td>{post.createdAt.toLocaleString()}</Td>
<Td>{post.content}</Td>
</Tr>
))}
</Tbody>
</Table>
</VStack>
</Flex>
</Flex>
</>
);
}
export async function getStaticProps() {
if (!global.mongo.client) {
global.mongo.client = new MongoClient(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
await global.mongo.client.connect();
}
const db = await global.mongo.client.db(process.env.DB_NAME);
const posts = await db.collection("posts").find({}).toArray();
if (!posts) {
return {
notFound: true,
};
}
return { props: { posts, user } };
}
일단 Post 2개가 있습니다.
그런데 이 Post는 누가 썼는지 알 수 없을까요?
여기서 문제가 생깁니다.
Static Site Generation 에서는 getStaticProps NextJS의 고유 함수를 이용해 Server 쪽 작업을 수행할 수 있는데,
위 코드에서 보면 db에 접속하여 일단 posts 만 가져오는 로직입니다.
posts 에 대한 각각의 User 정보를 가져올려면
posts를 가져오고 다시 각각의 posts에 해당하는 creatorId에 대한 user.name을 가져와야 되는데
그럴러면 다시 /api/users/[userId] 라우팅을 이용해야 되는데 이렇게는 작업이 불가능합니다.
그래서 Static Site Generation 에서는 한번의 서버 작업으로만 가져올 수 있는 데이터만 보내야 됩니다.
그래서 이럴때는 Post를 서버에 저장할때 creatorName 항목을 미리 저장하는 방법이 있습니다.
/pages/api/posts/index.js 파일을 다음과 같이 바꾸면 됩니다.
handler.post(async (req, res) => {
if (!req.user) {
return res.status(401).send("unauthenticated");
}
if (!req.body.content)
return res.status(400).send("You must write something");
const post = await insertPost(req.db, {
content: req.body.content,
creatorId: req.user._id,
creatorName: req.user.name, // 추가된 부분
});
return res.json({ post });
});
export default handler;
creatorName을 저장했습니다.
그리고 insertPost 함수에서 직접 creatorName부분도 추가 해야겠죠.
/db/post.js 부분에서 insertPost 함수만 다음과 같이 수정하시면 됩니다.
export async function insertPost(db, { content, creatorId, creatorName }) {
return db.collection('posts').insertOne({
_id: nanoid(12),
content,
creatorId,
creatorName, // 추가된 부분, 위에 함수 인자에도 추가됐습니다.
createdAt: new Date(),
}).then(({ ops }) => ops[0]);
}
그럼 처음부터 다시 해볼까요?
정상적으로 USER 부분에 작성자 이름이 나옵니다.
여기까지가 Static Site Generation 부분이었습니다.
SSG 방식은 NextJS가 처음 빌드 될 시 정적으로 페이지가 작성되기 때문에 웹에서 볼때 엄청 빠른 로딩효과를 누릴 수 있습니다.
위와 같은 DB 자료 수집 문제만 아니라면 간단한 DB 작업은 무조건 SSG 방식을 이용하는 게 전체적인 속도 측면에서 유리하니 참고 바랍니다.
Incremental Static Regeneration
ISR는 Static Site Generation 이 NextJS 앱이 처음 빌드 될때 한번 작성되는 단점을 보완한 방식인데요.
getStaticProps 함수에다가 return되는 props외에 revalidate 항목에 몇 초를 지정하면 그 몇 초마다 페이지가 생성되는 방식입니다.
그러면 ISR 방식의 페이지를 만들겠습니다.
/pages/list-isr.js
import React, { useState, useEffect } from "react";
import Head from "next/head";
import Error from "next/error";
import {
Flex,
VStack,
Heading,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
} from "@chakra-ui/react";
import { MongoClient } from "mongodb";
global.mongo = global.mongo || {};
export default function ListPostsPage({ posts }) {
const title = "List with Static Site Generation";
if (!posts) return <Error statusCode={404} />;
return (
<>
<Head>
<title>{title}</title>
</Head>
<Flex justify="center">
<Flex w="full" p={[0, 4, 6]}>
<VStack w="full" h="full" spacing={5}>
<Heading mb={2}>Total Posts : {posts.length} </Heading>
<Table size="sm" mb={12}>
<Thead>
<Tr>
<Th>Time</Th>
<Th>User</Th>
<Th>Contents</Th>
</Tr>
</Thead>
<Tbody>
{posts.map((post) => (
<Tr key={post.createdAt.toString()}>
<Td>{post.createdAt.toLocaleString()}</Td>
<Td>{post.creatorName}</Td>
<Td>{post.content}</Td>
</Tr>
))}
</Tbody>
</Table>
</VStack>
</Flex>
</Flex>
</>
);
}
export async function getStaticProps() {
if (!global.mongo.client) {
global.mongo.client = new MongoClient(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
await global.mongo.client.connect();
}
const db = await global.mongo.client.db(process.env.DB_NAME);
const posts = await db.collection("posts").find({}).toArray();
if (!posts) {
return {
notFound: true,
};
}
return { props: { posts }, revalidate: 60*60*24 };
}
SSG 파일과 같습니다.
다만 마지막에 getStaticProps 함수에서 return 할때 revalidate 부분만 60*60*24초로 지정했습니다.
즉, 하루마다 갱신하라는 얘기입니다.
Server Side Rendering
이제 NextJS 앱의 백미 서버 사이드 렌더링에 대해 알아 보겠습니다.
import React, { useState, useEffect } from "react";
import Head from "next/head";
import Error from "next/error";
import { all } from "@/middlewares/index";
import { useCurrentUser } from "@/hooks/index";
import { findPostsAll } from "@/db/index";
import {
Flex,
VStack,
Heading,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
} from "@chakra-ui/react";
export default function ListPostsPage({ posts }) {
const title = "List with Server Side Rendering";
const [user] = useCurrentUser();
if (!posts) return <Error statusCode={404} />;
return (
<>
<Head>
<title>{title}</title>
</Head>
<Flex justify="center">
<Flex w="full" p={[0, 4, 6]}>
<VStack w="full" h="full" spacing={5}>
<Heading mb={2}>Total Posts : {posts.length} </Heading>
<Table size="sm" mb={12}>
<Thead>
<Tr>
<Th>Time</Th>
<Th>User</Th>
<Th>Contents</Th>
</Tr>
</Thead>
<Tbody>
{posts.map((post) => (
<Tr key={post.createdAt.toString()}>
<Td>{post.createdAt.toLocaleString()}</Td>
<Td>{post.creatorName}</Td>
<Td>{post.content}</Td>
</Tr>
))}
</Tbody>
</Table>
</VStack>
</Flex>
</Flex>
</>
);
}
export async function getServerSideProps(context) {
await all.run(context.req, context.res);
const posts = await findPostsAll(context.req.db);
if (!posts) context.res.statusCode = 404;
return { props: { posts } };
}
서버사이드 렌더링은 getServerSideProps(context) 함수를 이용하는데요.
여기에 context 인자를 넘길 수 있습니다.
여기 context에는 request, response가 들어있습니다.
그래서 all.run(context.req, context.req) 를 실행하면 우리가 만든 DB 함수를 활용할 수 있게 됩니다.
getStaticProps에서는 context함수를 전달하지 못해 직접 MongoDB를 불러와서 작업한 거랑 천지 차지입니다.
getServerSideProps함수에서 우리는 findPostsAll이란 DB 함수를 이용해 모든 Post를 불러옵니다.
findPostsAll 함수를 볼까요?
/db/post.js
export async function findPostsAll(db) {
return db.collection("posts").find({}).toArray();
}
간단합니다.
db.collection("posts")에서 find 명령어를 빈객체로 전달하고 toArray함수로 배열로 리턴받으면 끝입니다.
테스트한게 많이 보이네요.
그럼 여기서 한가지 더 나아갈게 있는데요.
바로 Post 삭제입니다.
Post 삭제 기능을 서버사이드 렌더링 페이지에 추가해 보겠습니다.
일단 UI 부분에 X 버튼을 추가해서 삭제 로직을 달아 보겠습니다.
import React, { useState } from "react";
import Head from "next/head";
import Router from "next/router";
import Error from "next/error";
import { all } from "@/middlewares/index";
import { findPostsAll } from "@/db/index";
import {
Flex,
VStack,
Heading,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
useDisclosure,
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
AlertDialogCloseButton,
Button,
} from "@chakra-ui/react";
export default function ListPostsPage({ posts }) {
const title = "List with Server Side Rendering";
const [isUpdating, setIsUpdating] = useState(false);
const { isOpen, onOpen, onClose } = useDisclosure();
const cancelRef = React.useRef();
if (!posts) return <Error statusCode={404} />;
async function handleSubmit(_id) {
if (isUpdating) return;
setIsUpdating(true);
const body = {
_id: _id,
};
const res = await fetch("/api/posts/handle", {
method: "PATCH",
body: JSON.stringify(body),
});
setIsUpdating(false);
Router.reload(window.location.pathname);
}
return (
<>
<Head>
<title>{title}</title>
</Head>
<Flex justify="center">
<Flex w="full" p={[0, 4, 6]}>
<VStack w="full" h="full" spacing={5}>
<Heading mb={2}>Total Posts : {posts.length} </Heading>
<Table size="sm" mb={12}>
<Thead>
<Tr>
<Th>Del</Th>
<Th>Time</Th>
<Th>User</Th>
<Th>Contents</Th>
</Tr>
</Thead>
<Tbody>
{posts.map((post) => (
<Tr key={post.createdAt.toString()}>
<Td>
<Button size="sm" onClick={onOpen}>
X
</Button>
<AlertDialog
motionPreset="slideInBottom"
leastDestructiveRef={cancelRef}
onClose={onClose}
isOpen={isOpen}
isCentered
>
<AlertDialogOverlay />
<AlertDialogContent>
<AlertDialogHeader>Delete?</AlertDialogHeader>
<AlertDialogCloseButton />
<AlertDialogBody>
Are you sure for delete?
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
No
</Button>
<Button
colorScheme="red"
ml={3}
onClick={(e) => {
e.preventDefault();
handleSubmit(post._id);
}}
>
Yes
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Td>
<Td>{post.createdAt.toLocaleString()}</Td>
<Td>{post.creatorName}</Td>
<Td>{post.content}</Td>
</Tr>
))}
</Tbody>
</Table>
</VStack>
</Flex>
</Flex>
</>
);
}
export async function getServerSideProps(context) {
await all.run(context.req, context.res);
const posts = await findPostsAll(context.req.db);
if (!posts) context.res.statusCode = 404;
return { props: { posts } };
}
Chakra-UI의 AlertDialog를 이용했으며,
X 버튼을 누르면 아래와 같이 Delete 화면이 보이게 됩니다.
그리고 Yes 버튼을 누르면 Post가 삭제되게 됩니다.
위 코드에서 보시면 Post 삭제는 /api/posts/handle 라우팅을 이용했습니다.
/api/posts/handle.js
import nc from "next-connect";
import { all } from "@/middlewares/index";
import { findOneAndDeleteById } from "@/db/index";
const handler = nc();
handler.use(all);
handler.patch(async (req, res) => {
if (!req.user) {
req.status(401).end();
return;
}
const { _id } = JSON.parse(req.body);
await findOneAndDeleteById(req.db, _id);
res.end("ok");
});
export default handler;
이 라우팅은 findOneAndDeleteById 함수를 불러오는데요. 다음과 같습니다.
/db/post.js
export async function findOneAndDeleteById(db, postId) {
return db
.collection("posts")
.findOneAndDelete({
_id: postId,
})
.then((post) => post || null);
}
MongoDB의 findOneAndDelete 함수를 이용하는 간단한 로직입니다.
이렇게 해서 NextJS와 MongoDB를 이용한 서버사이드 렌더링, 정적 사이트 생성등에 대한 강좌를 마치도록 하겠습니다.
'코딩 > React' 카테고리의 다른 글
ElectronJS 일렉트론 강좌 2편 SQLite3 sql.js (0) | 2021.09.18 |
---|---|
ElectronJS 일렉트론 강좌 1편 SQLite3 sql.js (4) | 2021.09.18 |
NextJS와 MongoDB 4편 Chakra-UI 적용하기 (0) | 2021.09.15 |
NextJS Data 가져오는 방법 (SSG, SSR, ISR) (3) | 2021.09.12 |
NextJS와 MongoDB로 유저 로그인 세션 구현하기 3편 (2) | 2021.09.05 |