개요
블로그를 직접 만들어보고 싶기도 하고 Next.js 에 대한 이해도를 높이기 위해 블로그를 만들어보며 이해하였습니다.
기능 구현
- 블로그 글 생성, 보기 기능 구현
- 카테고리 글 분류
- 태그 글 분류
- 챗봇 구현
- chatGPT 에 블로그 글 목록을 학습시킨 후 질문에 맞는 글 페이지와 설명 보여주기
- 그 외의 질문도 가능
- 블로그 sitemap.xml 생성 과 robots.txt 생성, feed.xml 생성 (SEO)
- 성능 최적화 시키기 (Light house)

왼쪽은 App Router로 최적화 진행 후
오른쪽은 Pages Router 최적화 하기 전
성능최적화 라이트 하우스 지표

블로그 글 View, Editor 구현
@uiw/react-md-editor
와 @uiw/react-markdown-preview
를 이용하였는데
생각보다 이쁘게 나오지 않아서 찾아보고 바꿀 예정이다.
아래 코드처럼 generateStaticParams
로 빌드타임에 정적으로 생성하게 진행하였고,
SEO
를 위해 메타데이터 주입을 아래 title
, description
, openGraph
를 생성하였다.
import PostPage from '@/components/PostPage';
import { getPost } from '@/utils/fetch';
import { createClient } from '@/utils/supabase/server';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
export default async function Post({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
if (!post) return notFound();
return <PostPage {...post} />;
}
export const generateStaticParams = async () => {
const supabase = createClient();
const { data } = await supabase.from('Post').select('id');
return data?.map(({ id }) => ({ id: id.toString() })) ?? [];
};
type PostProps = { params: { id: string } };
export const generateMetadata = async ({
params
}: PostProps): Promise<Metadata> => {
const post = await getPost(params.id);
return {
title: post?.title,
description: post?.content?.split('.')[0],
openGraph: post?.preview_image_url
? {
images: [
{
url: post?.preview_image_url
}
]
}
: undefined
};
};
아래와 같이 컴포넌트로 만들어서 사용.
export const MarkdownEditor = ({ ...rest }: MDEditorProps) => (
<div data-color-mode="light">
<MDEditor {...rest} />
</div>
);
export const MarkdownViewer = ({ ...rest }: MarkdownPreviewProps) => (
<div data-color-mode="light">
<MDViewer {...rest} />
</div>
);
카테고리, 태그 글 분류
카테고리를 클릭했을 경우 해당 카테고리에 해당하는 페이지 리스트를 보여줘야 하므로 category 에 해당하는 글을 가져오게 진행 후 컴포넌트를 만들었다.
태그 또한 마찬가지.
이 또한 메타데이터 주입과 정적생성을 진행.
import PostList from '@/components/PostList';
import { getCategories, getPosts } from '@/utils/fetch';
import { Metadata } from 'next';
export default async function CategoryPosts({
params
}: {
params: { category: string };
}) {
const category = decodeURIComponent(params.category);
const posts = await getPosts({ category });
return (
<PostList category={decodeURIComponent(category)} initalPosts={posts} />
);
}
export const generateStaticParams = async () => {
const categories = await getCategories();
return categories.map((category) => ({ category }));
};
type CategoryPageProps = { params: { category: string } };
export const generateMetadata = async ({
params
}: CategoryPageProps): Promise<Metadata> => {
return {
title: `toris-dev의 블로그 - ${decodeURIComponent(params.category)}`,
description: '프로젝트 이야기를 나누는 블로그입니다.'
};
};
챗봇(chatGPT-3.5) 구현

Open AI API 를 가져와서 role 을 system으로 한 후 블로그 글 학습을 진행.
기술 블로그를 들어오는 이유는 모르는 걸 찾아보기 위해 들어오기 위함이라고 생각하여 추후에
챗봇을 페이지로 만드는게 아니라 우측하단에 아이콘을 클릭하면 채팅을 진행하게 진행.
api/completions 를 만들어서 백엔드에서 처리 하게 진행.
import { createClient } from '@/utils/supabase/server';
import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';
import OpenAI from 'openai';
import type {
ChatCompletionMessageParam,
ChatCompletionSystemMessageParam
} from 'openai/resources/index.mjs';
const openai = new OpenAI({
apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY
});
const getFirstMessage = async (
supabase: ReturnType<typeof createClient>
): Promise<ChatCompletionSystemMessageParam> => {
const { data: postMetadataList } = await supabase
.from('Post')
.select('id, title, category, tags');
return {
role: 'system',
content: `너는 개발 전문 챗봇이야. 블로그 글을 참고하여 상대방의 질문에 답변해줘야 해.
너가 잘 모르는 질문이라면, 다음 블로그 글들을 참고하여 답변해줘.
[블로그 글 목록]
${JSON.stringify(postMetadataList ?? [])}
너는 retrieve 함수를 사용하여 블로그 글을 가져올 수 있어. 참고하고 싶은 블로그 글이 있다면, retrieve 함수를 사용하여 블로그 글을 가져와서 답변해줘.`
};
};
const getBlogContent = async (
id: string,
supabase: ReturnType<typeof createClient>
) => {
const { data } = await supabase.from('Post').select('*').eq('id', id);
if (!data) return {};
return data[0];
};
export async function POST(request: NextRequest) {
const { messages } = (await request.json()) as {
messages: ChatCompletionMessageParam[];
};
const supabase = createClient(cookies());
if (messages.length === 1) {
messages.unshift(await getFirstMessage(supabase));
}
while (messages.at(-1)?.role !== 'assistant') {
const response = await openai.chat.completions.create({
messages,
model: 'gpt-3.5-turbo',
function_call: 'auto',
functions: [
{
name: 'retrieve',
parameters: {
type: 'object',
properties: {
id: {
type: 'string',
description: '가져올 블로그 글의 id'
}
}
},
description: '특정 id를 가진 블로그 글의 전체 내용을 가져옵니다.'
}
]
});
const responseMessage = response.choices[0].message;
if (responseMessage.function_call) {
const { id } = JSON.parse(responseMessage.function_call.arguments);
const functionResult = await getBlogContent(id, supabase);
messages.push({
role: 'function',
content: JSON.stringify(functionResult),
name: responseMessage.function_call.name
});
} else {
messages.push(responseMessage);
}
}
return Response.json({ messages });
}
블로그 sitemap.xml 생성 과 robots.txt 생성, feed.xml 생성 (SEO)
sitemap.xml 생성
블로그 글, tags, search sitemap 에 등록
import { getPostId } from '@/utils/fetch';
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPostId();
const sitemap = posts.map(({ id, created_at }) => {
return {
url: `https://toris-blog.vercel.app/posts/${id}`,
lastModified: new Date(created_at)
};
});
return [
{
url: 'https://toris-blog.vercel.app',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1
},
{
url: 'https://toris-blog.vercel.app/search',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.8
},
{
url: 'https://toris-blog.vercel.app/tags',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.5
},
...sitemap
];
}
robots.txt
/admin 페이지를 제외하고 검색엔진에 정보수집 허용
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/admin'
},
sitemap: 'https://toris-blog.vercel.app/sitemap.xml'
};
}
성능 최적화하기
Next.js 자체에서 최적화를 도와주기 때문에 어렵지 않다.
성능에 문제가 되는 것은 이미지, 폰트, 아이콘 이였고
이미지는 크기와 prority 태그를 붙여줬다.
<Image
src={preview_image_url ?? '/book-open.svg'}
fill
sizes="360px"
alt={title}
className="object-cover"
priority
/>
폰트는 next에서 font를 자체적으로 제공해주기 때문에 구글 latin 을 사용하였다.
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
아이콘은 react-icons 를 사용하고 있었는데 500KB를 넘어서는 문제가 있었다.
번들링 될 때 사용하는 모든 아이콘이 포함되어 청크사이즈가 너무 커지게 된다. 모든 아이콘이 번들링이 되는 것 이었다.
yarn remove react-icons
를 삭제하고 yarn add @react-icons/all-files
설치하여 번들링 크기를 줄였다. ( 아이콘은 동일하다. )
부족한 점
UI/UX가 너무 부족한 상태이다. 또한 댓글, 방문록, 글 수정을 추가구현 해야한다.
Next.js 가 이렇게 많은 기능을 제공하는지 다시 깨달았고 훌륭한 라이브러리와 서비스가 있다는 것에 감탄했다.
만들면서 이게 맞는건지 했지만 만족하는 시간이었다.
'Next.js' 카테고리의 다른 글
[Next.JS] parallel routes interception 트러블 슈팅 (0) | 2024.07.09 |
---|---|
[Next.JS] routes handler 이용하여 socket.io 연결 (0) | 2024.05.03 |
[프로젝트] 개인 블로그 챗봇 Open API 사용하기 (0) | 2024.03.19 |
[엘리스 SW 스터디] Next.js 기초와 내장 컴포넌트 (1) | 2024.01.21 |
Next.js 기초!!! (1) | 2024.01.13 |
개요
블로그를 직접 만들어보고 싶기도 하고 Next.js 에 대한 이해도를 높이기 위해 블로그를 만들어보며 이해하였습니다.
기능 구현
- 블로그 글 생성, 보기 기능 구현
- 카테고리 글 분류
- 태그 글 분류
- 챗봇 구현
- chatGPT 에 블로그 글 목록을 학습시킨 후 질문에 맞는 글 페이지와 설명 보여주기
- 그 외의 질문도 가능
- 블로그 sitemap.xml 생성 과 robots.txt 생성, feed.xml 생성 (SEO)
- 성능 최적화 시키기 (Light house)

왼쪽은 App Router로 최적화 진행 후
오른쪽은 Pages Router 최적화 하기 전
성능최적화 라이트 하우스 지표

블로그 글 View, Editor 구현
@uiw/react-md-editor
와 @uiw/react-markdown-preview
를 이용하였는데
생각보다 이쁘게 나오지 않아서 찾아보고 바꿀 예정이다.
아래 코드처럼 generateStaticParams
로 빌드타임에 정적으로 생성하게 진행하였고,
SEO
를 위해 메타데이터 주입을 아래 title
, description
, openGraph
를 생성하였다.
import PostPage from '@/components/PostPage';
import { getPost } from '@/utils/fetch';
import { createClient } from '@/utils/supabase/server';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
export default async function Post({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
if (!post) return notFound();
return <PostPage {...post} />;
}
export const generateStaticParams = async () => {
const supabase = createClient();
const { data } = await supabase.from('Post').select('id');
return data?.map(({ id }) => ({ id: id.toString() })) ?? [];
};
type PostProps = { params: { id: string } };
export const generateMetadata = async ({
params
}: PostProps): Promise<Metadata> => {
const post = await getPost(params.id);
return {
title: post?.title,
description: post?.content?.split('.')[0],
openGraph: post?.preview_image_url
? {
images: [
{
url: post?.preview_image_url
}
]
}
: undefined
};
};
아래와 같이 컴포넌트로 만들어서 사용.
export const MarkdownEditor = ({ ...rest }: MDEditorProps) => (
<div data-color-mode="light">
<MDEditor {...rest} />
</div>
);
export const MarkdownViewer = ({ ...rest }: MarkdownPreviewProps) => (
<div data-color-mode="light">
<MDViewer {...rest} />
</div>
);
카테고리, 태그 글 분류
카테고리를 클릭했을 경우 해당 카테고리에 해당하는 페이지 리스트를 보여줘야 하므로 category 에 해당하는 글을 가져오게 진행 후 컴포넌트를 만들었다.
태그 또한 마찬가지.
이 또한 메타데이터 주입과 정적생성을 진행.
import PostList from '@/components/PostList';
import { getCategories, getPosts } from '@/utils/fetch';
import { Metadata } from 'next';
export default async function CategoryPosts({
params
}: {
params: { category: string };
}) {
const category = decodeURIComponent(params.category);
const posts = await getPosts({ category });
return (
<PostList category={decodeURIComponent(category)} initalPosts={posts} />
);
}
export const generateStaticParams = async () => {
const categories = await getCategories();
return categories.map((category) => ({ category }));
};
type CategoryPageProps = { params: { category: string } };
export const generateMetadata = async ({
params
}: CategoryPageProps): Promise<Metadata> => {
return {
title: `toris-dev의 블로그 - ${decodeURIComponent(params.category)}`,
description: '프로젝트 이야기를 나누는 블로그입니다.'
};
};
챗봇(chatGPT-3.5) 구현

Open AI API 를 가져와서 role 을 system으로 한 후 블로그 글 학습을 진행.
기술 블로그를 들어오는 이유는 모르는 걸 찾아보기 위해 들어오기 위함이라고 생각하여 추후에
챗봇을 페이지로 만드는게 아니라 우측하단에 아이콘을 클릭하면 채팅을 진행하게 진행.
api/completions 를 만들어서 백엔드에서 처리 하게 진행.
import { createClient } from '@/utils/supabase/server';
import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';
import OpenAI from 'openai';
import type {
ChatCompletionMessageParam,
ChatCompletionSystemMessageParam
} from 'openai/resources/index.mjs';
const openai = new OpenAI({
apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY
});
const getFirstMessage = async (
supabase: ReturnType<typeof createClient>
): Promise<ChatCompletionSystemMessageParam> => {
const { data: postMetadataList } = await supabase
.from('Post')
.select('id, title, category, tags');
return {
role: 'system',
content: `너는 개발 전문 챗봇이야. 블로그 글을 참고하여 상대방의 질문에 답변해줘야 해.
너가 잘 모르는 질문이라면, 다음 블로그 글들을 참고하여 답변해줘.
[블로그 글 목록]
${JSON.stringify(postMetadataList ?? [])}
너는 retrieve 함수를 사용하여 블로그 글을 가져올 수 있어. 참고하고 싶은 블로그 글이 있다면, retrieve 함수를 사용하여 블로그 글을 가져와서 답변해줘.`
};
};
const getBlogContent = async (
id: string,
supabase: ReturnType<typeof createClient>
) => {
const { data } = await supabase.from('Post').select('*').eq('id', id);
if (!data) return {};
return data[0];
};
export async function POST(request: NextRequest) {
const { messages } = (await request.json()) as {
messages: ChatCompletionMessageParam[];
};
const supabase = createClient(cookies());
if (messages.length === 1) {
messages.unshift(await getFirstMessage(supabase));
}
while (messages.at(-1)?.role !== 'assistant') {
const response = await openai.chat.completions.create({
messages,
model: 'gpt-3.5-turbo',
function_call: 'auto',
functions: [
{
name: 'retrieve',
parameters: {
type: 'object',
properties: {
id: {
type: 'string',
description: '가져올 블로그 글의 id'
}
}
},
description: '특정 id를 가진 블로그 글의 전체 내용을 가져옵니다.'
}
]
});
const responseMessage = response.choices[0].message;
if (responseMessage.function_call) {
const { id } = JSON.parse(responseMessage.function_call.arguments);
const functionResult = await getBlogContent(id, supabase);
messages.push({
role: 'function',
content: JSON.stringify(functionResult),
name: responseMessage.function_call.name
});
} else {
messages.push(responseMessage);
}
}
return Response.json({ messages });
}
블로그 sitemap.xml 생성 과 robots.txt 생성, feed.xml 생성 (SEO)
sitemap.xml 생성
블로그 글, tags, search sitemap 에 등록
import { getPostId } from '@/utils/fetch';
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPostId();
const sitemap = posts.map(({ id, created_at }) => {
return {
url: `https://toris-blog.vercel.app/posts/${id}`,
lastModified: new Date(created_at)
};
});
return [
{
url: 'https://toris-blog.vercel.app',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1
},
{
url: 'https://toris-blog.vercel.app/search',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.8
},
{
url: 'https://toris-blog.vercel.app/tags',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.5
},
...sitemap
];
}
robots.txt
/admin 페이지를 제외하고 검색엔진에 정보수집 허용
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/admin'
},
sitemap: 'https://toris-blog.vercel.app/sitemap.xml'
};
}
성능 최적화하기
Next.js 자체에서 최적화를 도와주기 때문에 어렵지 않다.
성능에 문제가 되는 것은 이미지, 폰트, 아이콘 이였고
이미지는 크기와 prority 태그를 붙여줬다.
<Image
src={preview_image_url ?? '/book-open.svg'}
fill
sizes="360px"
alt={title}
className="object-cover"
priority
/>
폰트는 next에서 font를 자체적으로 제공해주기 때문에 구글 latin 을 사용하였다.
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
아이콘은 react-icons 를 사용하고 있었는데 500KB를 넘어서는 문제가 있었다.
번들링 될 때 사용하는 모든 아이콘이 포함되어 청크사이즈가 너무 커지게 된다. 모든 아이콘이 번들링이 되는 것 이었다.
yarn remove react-icons
를 삭제하고 yarn add @react-icons/all-files
설치하여 번들링 크기를 줄였다. ( 아이콘은 동일하다. )
부족한 점
UI/UX가 너무 부족한 상태이다. 또한 댓글, 방문록, 글 수정을 추가구현 해야한다.
Next.js 가 이렇게 많은 기능을 제공하는지 다시 깨달았고 훌륭한 라이브러리와 서비스가 있다는 것에 감탄했다.
만들면서 이게 맞는건지 했지만 만족하는 시간이었다.
'Next.js' 카테고리의 다른 글
[Next.JS] parallel routes interception 트러블 슈팅 (0) | 2024.07.09 |
---|---|
[Next.JS] routes handler 이용하여 socket.io 연결 (0) | 2024.05.03 |
[프로젝트] 개인 블로그 챗봇 Open API 사용하기 (0) | 2024.03.19 |
[엘리스 SW 스터디] Next.js 기초와 내장 컴포넌트 (1) | 2024.01.21 |
Next.js 기초!!! (1) | 2024.01.13 |