직접 개발하여 사용하고 있고, 운영하고 있습니다 👋
Next.js + typescript 의 블로그를 소개합니다.
-
blog Template : https://github.com/Choi-HyunHo/blog-template
이전까지 naver, tistory, velog, gatsby 등 여러 블로그를 사용해봤습니다.
그 중 최근까지 사용한 블로그는 gatsby 로 된 블로그였는데 해당 블로그는 직접 만들지 않고
이미 만들어져있는 템플릿을 가져다 글만 작성한 것에 불과했습니다.
그러던 중 결심을 하게 된 계기가
-
종종 터지는 오류들
(물론 어떻게든 해결을 했습니다만.. 잘 되다가 호환성 등 갑자기 발생하여 어디서 무슨 이유로 발생하는 건지 원인을 알기는 어려웠습니다) -
앞으로 블로그를 통해 정리를 할 건데, 내 맘대로 커스텀을 하고 싶은 욕구
-
직접 만들어보는 것이 더 실력 향상에도 좋을 것 같다
-
글 쓰는 것 이외에도 블로그 자체를 꾸준히 업그레이드 하자
위의 4가지 생각을 하면서 이번 블로그를 만들게 되었습니다.
-
다크모드를 적용하자 → 밤에도 글을 쉽게 볼 수 있도록
-
나를 나타내자 → 이력서를 블로그에 포함시켜서 기타 블로그와 차별점을 두자
-
많은 기능보다는 블로그에 필요한 기능만 만들자 → 해당 블로그를 사용하는 누구나 커스텀이 쉽도록
-
검색 엔진 최적화 → 각 페이지 마다 metedata 가 설정되어 있습니다.
-
모바일 화면까지 반응형을 지원 합니다.
- Clone the reop
https://github.com/Choi-HyunHo/blog.git
- Install YARN packages
yarn install
- Start
yarn dev
Next.js 13 버전의 App Directory
를 사용했습니다.
.
├── README.md
├── contentlayer.config.ts # next-contentlayer
├── next-env.d.ts
├── next.config.js # next 설정
├── package-lock.json
├── package.json
├── postcss.config.js
├── posts # 포스팅 하는 글 쓰는 폴더(.mdx)
├── public # 사용되는 이미지들
│ └── images
│ ├── logo.png
│ ├── overview.gif
│ └── 404.jpg
│ └── overview.jpg
│ └── profile.jpg
│ └── human.jpg
│
├── src
│ ├── app
│ │ ├── api
│ │ │ └── contact
│ │ │ └── route.ts # email API
│ │ ├── robots.ts # metadata
│ │ ├── sitemap.ts # metadata
│ | ├── not-found.tsx # 404 커스텀 페이지
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx # 전반적인 레이아웃
│ │ ├── page.tsx # 메인 페이지
│ │ ├── posts
│ │ │ ├── [tag]
│ │ │ │ └── page.tsx
│ │ │ │ └── [slug]
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx # /posts 페이지
│ │ ├── resume
│ │ │ └── page.tsx # /resume 페이지
│ │ └── contact
│ │ └── page.tsx # /contact 페이지
│ ├── components
│ │ ├── Provider.tsx # next-themes Provider
│ │ ├── posts # /posts, /posts/[slug] 관련 컴포넌트
│ │ │ ├── BlogPost.tsx
│ │ │ ├── FeaturePost.tsx
│ │ │ ├── MainView.tsx
│ │ │ ├── Recent.tsx
│ │ │ └── Tags.tsx
│ │ │ └── TagView.tsx # /post/[tag] 페이지 컴포넌트
│ │ ├── resume # /resume 컴포넌트
│ │ │ └── Notion.tsx
│ │ ├── contact # /contact 컴포넌트
│ │ │ └── ContactForm.tsx
│ │ └── ui # UI 컴포넌트
│ │ ├── Button.tsx
│ │ ├── Footer.tsx
│ │ ├── Header.tsx
│ │ ├── Hero.tsx
│ │ └── Title.tsx
│ │ └── Banner.tsx
│ └── service # 서비스 관련 API
│ ├── email.ts
│ ├── nodemail.ts
│ ├── notion.ts
│ └── posts.ts
├── tailwind.config.js # tailwind 설정
├── tsconfig.json # typescript 설정
└── yarn.lock
-
Next.js에서 쉽게 테마를 전환할 수 있도록 도와주는 패키지입니다.(시스템 테마로도 업데이트 가능)
-
기본적으로 next 13 버전에서는 tailwind 를 초기 설치 때 지원을 합니다.
yarn add next-themes
// 사용 아이콘
yarn add react-icons
"use client";
import { ThemeProvider } from "next-themes";
export default function Provider({ children }: { children: React.ReactNode }) {
return <ThemeProvider attribute="class">{children}</ThemeProvider>;
}
위를 통해서 해당 children은 일관된 테마를 유지가 가능합니다.
여기서 핵심은 "use client"
를 넣어줘야합니다.
-
csr로 작동을 해야 ThemeProvider가 작동을 합니다.
-
이때
attribute="class"
를 추가해줌으로써 className에 dark와 light가 토글이 됩니다.
전체 레이아웃 입니다.
<html lang="ko">
<body className={karla.className}>
<Provider> // ✅
<Header />
{children}
<Footer />
</Provider>
</body>
</html>
"use client";
import React, { useState, useEffect } from "react";
import { useTheme } from "next-themes"; // ✅
import { BsFillSunFill, BsFillMoonFill } from "react-icons/bs";
const Button = () => {
const [mounted, setMounted] = useState(false);
const { systemTheme, theme, setTheme } = useTheme(); // ✅
const currentTheme = theme === "system" ? systemTheme : theme; // ✅
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
const onClick = (mode: string) => () => {
setTheme(mode);
};
return (
<>
{currentTheme === "dark" ? (
<BsFillMoonFill
onClick={onClick("light")} // ✅
className="cursor-pointer"
/>
) : (
<BsFillSunFill
onClick={onClick("dark")} // ✅
className="cursor-pointer"
/>
)}
</>
);
};
export default Button;
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class"
};
클래스 안에 dark:
를 사용해서 색상 및 효과를 바꿀 수 있습니다.
기본 html 컨텐츠에 스타일을 추가해주는 tailwind 플러그인
yarn add -D @tailwindcss/typography
노션의 이력서를 프로젝트로 불러오기 위해 사용 했습니다. 공식문서
yarn add react-notion
지금은 react-notion-x 라는 더 많은 기능을 지원하는 상위 버전이 있습니다.
- 하지만 단순히 페이지만 불러오는 용도로는 이전 react-notion 이 사용하기 쉽고 간편하다고 생각 했습니다.
NEXT_PUBLIC_NOTION_PAGE_ID = '노션 페이지 ID'
위의 ID는 Copy web link 를 누르면
아래와 같이 나옵니다.
https://actually-nemophila-cf3.notion.site/Choi-Hyun-Ho-e2fe264b22184e0785ef4af50cf47c16?pvs=4
그 중 e2fe264b22184e0785ef4af50cf47c16?pvs=4
이 부분만 가져옵니다.
해당 페이지의 데이터를 fetch API
를 통해서 불러옵니다.
export const getNotion = async () => {
const res = await fetch(
`https://notion-api.splitbee.io/v1/page/${process.env.NEXT_PUBLIC_NOTION_PAGE_ID}`
);
const data = await res.json();
return data;
};
- 해당 페이지에서 API 를 호출
import Notion from "@/components/resume/Notion";
import { getNotion } from "@/service/notion";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Resume",
description: "현호 이력서",
};
export default async function resume() {
const data = await getNotion(); // ✅
return <Notion data={data} />;
}
import "react-notion/src/styles.css";
import { NotionRenderer } from "react-notion";
export default function Notion({ data }: any) {
return <NotionRenderer blockMap={data} fullPage={true} hideHeader={true} />;
}
yarn add contentlayer next-contentlayer
import { defineDocumentType, makeSource } from "contentlayer/source-files";
export const Post = defineDocumentType(() => ({
name: "Post",
contentType: "mdx",
filePathPattern: `**/*.mdx`,
fields: {
title: { type: "string", required: true },
date: { type: "string", required: true },
description: { type: "string", required: true },
tag: { type: "string", required: true },
// ✅ 필요한 속성들 만들어서 추가
},
}));
export default makeSource({
contentDirPath: "posts",
documentTypes: [Post],
});
const { withContentlayer } = require("next-contentlayer");
module.exports = withContentlayer({
reactStrictMode: true,
});
제 블로그 기준 입니다.
- contentlayer.config.ts > Post 의 fields 와 동일한 형식으로 맞춰줍니다.
---
title: CSS-in-CSS 와 CSS-in-JS 에 대하여
date: "2023-04-11"
description: CSS를 어디서 사용하면 좋을까 ❓
tag: CSS
---
# 제목
- 마크다운 문법 사용하기
"compilerOptions": {
...
"paths": {
"contentlayer/generated": ["./.contentlayer/generated"]
}
},
"include": [
...
".contentlayer/generated"
],
설정이 올바르게 끝나고 재실행 하게 되면 아래와 같은 문구를 터미널에서 볼 수 있습니다.
Generated 86 documents in .contentlayer
- 작성된 글은 .contentlayer/generated/Post 에서 확인 할 수 있습니다.
---
title: CSS-in-CSS 와 CSS-in-JS 에 대하여
date: "2023-04-11"
description: CSS를 어디서 사용하면 좋을까 ❓
tag: CSS
---
이 부분의 tag 를 사용하게 되면 아래 코드에 추가해줘야 정상적으로 태그 별로 나눠집니다.
src/components/posts/Tags.tsx
const tagList = ["All", "Next.js", "React", "TS", "JS", "CSS"]; // 태그 목록 배열
sitemap은 구글, 네이버와 같은 검색 사이트들의 크롤링 봇들에게 우리 서비스에서 사용할 수 있는 사이트 주소를 알려주기 위해 활용 합니다.
이 부분은 직접 해보시는 것을 추천 드립니다(저도 아직 완벽하지는 않아서...)
yarn add next-sitemap -D
참고 : https://jforj.tistory.com/311
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: "https://www.choi-hyunho.com/",
generateRobotsTxt: true, // robots.txt generate 여부 (자동생성 여부)
sitemapSize: 7000, // sitemap별 최대 크기 (최대 크기가 넘어갈 경우 복수개의 sitemap으로 분리됨)
changefreq: "daily", // 페이지 주소 변경 빈도 (검색엔진에 제공됨) - always, daily, hourly, monthly, never, weekly, yearly 중 택 1
priority: 1, // 페이지 주소 우선순위 (검색엔진에 제공됨, 우선순위가 높은 순서대로 크롤링함)
robotsTxtOptions: {
// 정책 설정
policies: [
{
userAgent: "*", // 모든 agent 허용
allow: "/", // 모든 페이지 주소 크롤링 허용
disallow: [
"/exclude", // exclude로 시작하는 페이지 주소 크롤링 금지
],
},
// 추가 정책이 필요할 경우 배열 요소로 추가 작성
],
}, // robots.txt 옵션 설정
};
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"postbuild": "next-sitemap" // ✅ 추가
},
노션 페이지를 불러올 때 해당 화면 컴포넌트에서 useEffect 를 통해 가져오면 렌더링 될 때 마다 데이터를 불러오게 되어
사용자에게 깜빡이는 화면이 보이는 등 사용자 경험 측면에서 좋지 않았습니다.
이력서는 데이터가 자주 바뀌지 않는 화면이기 때문에 정적 페이지로 만들면 위의 이슈와 여러 이점들을 가져갈 수 있습니다.
next.js 는 정적 페이지를 만들 수 있기 때문에 빌드 시점에서 미리 렌더링해서 사용자 경험을 개선했습니다.
-
Next.js 13버전에서는
fetch API
를 사용하여 SSG, ISR, SSR 모두 구현이 가능 합니다. -
그 중 이력서 페이지는 SSG 로 되어있습니다.
sitemap은 구글, 네이버와 같은 검색 사이트들의 크롤링 봇들에게 우리 서비스에서 사용할 수 있는 사이트 주소를 알려주기 위해 활용 합니다.
블로그 만들 당시 next-sitemap
라이브러리를 사용해서 만들었습니다.
하지만 next 의 공식문서를 더 살펴보니까 sitemap 과 robots 을 자체적으로 사용할 수 있는 방법을 찾았습니다.
동적 sitemap 만드는 예시(v1.3.1)
import { MetadataRoute } from "next";
import { getPosts } from "@/service/posts";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const data = await getPosts();
const postUrls = data.map((post) => ({
url: `https://www.choi-hyunho.com/posts/${post._id}`,
lastModified: new Date(),
}));
return [
{
url: "https://www.choi-hyunho.com",
lastModified: new Date(),
},
{
url: "https://www.choi-hyunho.com/resume",
lastModified: new Date(),
},
{
url: "https://www.choi-hyunho.com/posts",
lastModified: new Date(),
},
...postUrls,
];
}
정리 : v1.4.2
- 유효성 검사 : yup
- 메일 전송 : nodemailer
정리 : v1.5.2
정리 : v1.6.2
순서대로 따라하면 어렵지 않게 할 수 있습니다.✌️