Next.js ๋ธ๋ก๊ทธ์ Notion ์ฐ๋ํ๊ธฐ
๋ธ๋ก๊ทธ๋ฅผ ์ด์ํ๋ค ๋ณด๋ฉด ๊ธ ์์ฑ์ ๋ ํธํ๊ฒ ํ๊ณ ์ถ์ ๋ง์์ด ์๊น๋๋ค. ๊ธฐ์กด์๋ ๋ก์ปฌ์์ ๋งํฌ๋ค์ด ํ์ผ์ ์์ฑํ๊ณ Git์ ์ปค๋ฐํ๋ ๋ฐฉ์์ด์๋๋ฐ, Notion์ ํธ๋ฆฌํ ์๋ํฐ๋ฅผ ํ์ฉํ๊ณ ์ถ์ด์ก์ต๋๋ค. ์ด๋ฒ ๊ธ์์๋ Next.js ๋ธ๋ก๊ทธ์ Notion์ ์ฐ๋ํ๋ฉด์ ๊ฒช์ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ๊ณต์ ํฉ๋๋ค.
์ Notion์ ์ ํํ๋
๊ธฐ์กด ๋ธ๋ก๊ทธ๋ MDX ํ์ผ์ data/blog/ ํด๋์ ์ ์ฅํ๋ ๋ฐฉ์์ด์์ต๋๋ค. ์ด ๋ฐฉ์์ ์ฅ์ ์ ๋ช
ํํ์ง๋ง, ๋ช ๊ฐ์ง ๋ถํธํ ์ ์ด ์์์ต๋๋ค:
- ๋ก์ปฌ ํ๊ฒฝ์์๋ง ๊ธ์ ์์ฑํ ์ ์์
- ์ด๋ฏธ์ง ์ ๋ก๋๊ฐ ๋ฒ๊ฑฐ๋ก์
- ์๋ํฐ ๊ธฐ๋ฅ์ด ์ ํ์
- ์ฌ๋ฌ ๊ธฐ๊ธฐ์์ ์์ ํ๊ธฐ ์ด๋ ค์
Notion์ ์ฌ์ฉํ๋ฉด ์ด๋ฐ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ ์ ์์ต๋๋ค. ํนํ Notion์ ๊ฐ๋ ฅํ ์๋ํฐ์ ํ์ ๊ธฐ๋ฅ์ ๋ธ๋ก๊ทธ ์ด์์ ํจ์ฌ ํธํ๊ฒ ๋ง๋ค์ด์ค๋๋ค.
๊ตฌํ ๊ณํ
Notion ์ฐ๋์ ์ํด ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ด ํ์ํ์ต๋๋ค:
- Notion โ MDX ๋๊ธฐํ: Notion์์ ์์ฑํ ๊ธ์ MDX ํ์ผ๋ก ๋ณํ
- MDX โ Notion ์ ๋ก๋: ๊ธฐ์กด MDX ํ์ผ์ Notion์ผ๋ก ์ ๋ก๋
- ์๋ ๋น๋ ํตํฉ: ๋น๋ ์ ์๋์ผ๋ก ์ต์ ๊ธ ๋๊ธฐํ
Notion API ์ค์
๋จผ์ Notion Integration์ ์์ฑํ๊ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ค์ ํด์ผ ํฉ๋๋ค.
Integration ์์ฑ
- Notion Integrations ํ์ด์ง์์ ์ Integration ์์ฑ
- Internal Integration Token ๋ณต์ฌ
- ๋ฐ์ดํฐ๋ฒ ์ด์ค์ Integration ์ฐ๊ฒฐ
๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ ๊ตฌ์ฑ
๋ธ๋ก๊ทธ ํฌ์คํธ์ ํ์ํ ์์ฑ๋ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ถ๊ฐํ์ต๋๋ค:
- Title (Title): ํฌ์คํธ ์ ๋ชฉ
- Date (Date): ๋ฐํ์ผ
- Tags (Multi-select): ํ๊ทธ ๋ชฉ๋ก
- Summary (Text): ์์ฝ
- Draft (Checkbox): ์ด์ ์ฌ๋ถ
- Authors (Multi-select): ์์ฑ์
์ฒซ ๋ฒ์งธ ๋ฌธ์ : databases.query๊ฐ ์๋ค?
Notion API ํด๋ผ์ด์ธํธ๋ฅผ ์ฌ์ฉํด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฟผ๋ฆฌํ๋ ค๊ณ ํ๋๋ฐ, notion.databases.query() ๋ฉ์๋๊ฐ ์กด์ฌํ์ง ์์์ต๋๋ค. ํ์ธํด๋ณด๋ @notionhq/client ํจํค์ง์ databases ๊ฐ์ฒด์๋ retrieve, create, update ๋ฉ์๋๋ง ์์์ต๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ
Notion API์ ๊ณต์ ์๋ํฌ์ธํธ๋ ์กด์ฌํ์ง๋ง ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๊ตฌํ๋์ด ์์ง ์์์ต๋๋ค. ์ง์ HTTP ์์ฒญ์ ๋ณด๋ด๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐํ์ต๋๋ค:
const queryResponse = await fetch(
`https://api.notion.com/v1/databases/${NOTION_DATABASE_ID}/query`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${NOTION_TOKEN}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
},
body: JSON.stringify(queryOptions),
}
)
์ด๋ ๊ฒ ํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํ์ด์ง๋ฅผ ์ฟผ๋ฆฌํ ์ ์์ต๋๋ค.
๋ ๋ฒ์งธ ๋ฌธ์ : properties๊ฐ undefined
๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์กฐํํ์ ๋ properties ํ๋๊ฐ undefined๋ก ๋์ค๋ ๊ฒฝ์ฐ๊ฐ ์์์ต๋๋ค. ์ด๋ Integration์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๋๋ก ์ฐ๊ฒฐ๋์ง ์์๊ฑฐ๋, ๊ถํ์ด ๋ถ์กฑํ ๋ ๋ฐ์ํฉ๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ
properties๊ฐ ์์ ๋๋ ๋์ํ๋๋ก ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ถ๊ฐํ์ต๋๋ค:
if (!database.properties) {
console.warn('โ ๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ์ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.')
// ๊ธฐ๋ณธ ์์ฑ๋ง ์ฌ์ฉํ์ฌ ๊ณ์ ์งํ
availableProperties = ['Title', 'Date']
} else {
availableProperties = Object.keys(database.properties)
}
์ด๋ ๊ฒ ํ๋ฉด Integration ์ฐ๊ฒฐ์ด ์๋ฃ๋์ง ์์๋ ๊ธฐ๋ณธ์ ์ธ ๋๊ธฐํ๋ ๊ฐ๋ฅํฉ๋๋ค.
์ธ ๋ฒ์งธ ๋ฌธ์ : ์ฝ๋ ๋ธ๋ก ์ธ์ด ํ์
MDX ํ์ผ์ ์ฝ๋ ๋ธ๋ก์ Notion์ผ๋ก ๋ณํํ ๋ ์ธ์ด ์ฝ๋๊ฐ Notion์ด ์ง์ํ๋ ํ์๊ณผ ๋ง์ง ์์์ต๋๋ค. ์๋ฅผ ๋ค์ด:
jsโjavascript๋ก ๋ณํ ํ์ts:file.tsโ ํ์ผ๋ช ์ ๊ฑฐํ๊ณtypescript๋ก ๋ณํtexโlatex๋ก ๋ณํ- ์ง์ํ์ง ์๋ ์ธ์ด โ
plain text๋ก ํด๋ฐฑ
ํด๊ฒฐ ๋ฐฉ๋ฒ
์ธ์ด ์ฝ๋๋ฅผ ์ ๊ทํํ๋ ํจ์๋ฅผ ๋ง๋ค์์ต๋๋ค:
function normalizeLanguage(lang) {
if (!lang) return 'plain text'
// ํ์ผ๋ช
ํ์ ์ ๊ฑฐ
lang = lang.split(':')[0].split('.')[0].toLowerCase().trim()
// ์ธ์ด ๋งคํ
const languageMap = {
'js': 'javascript',
'ts': 'typescript',
'py': 'python',
'tex': 'latex',
// ... ๋ ๋ง์ ๋งคํ
}
const normalized = languageMap[lang] || lang
// Notion์ด ์ง์ํ๋ ์ธ์ด์ธ์ง ํ์ธ
const supportedLanguages = ['javascript', 'typescript', 'python', ...]
return supportedLanguages.includes(normalized) ? normalized : 'plain text'
}
๋ค ๋ฒ์งธ ๋ฌธ์ : ๋ธ๋ก ๊ฐ์ ์ ํ
Notion API๋ ํ ๋ฒ์ ์ต๋ 100๊ฐ์ ๋ธ๋ก๋ง ์์ฑํ ์ ์์ต๋๋ค. ๊ธด ๊ธ์ ๊ฒฝ์ฐ ์ด ์ ํ์ ์ด๊ณผํ ์ ์์ต๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ
๋ธ๋ก์ 100๊ฐ์ฉ ๋๋ ์ ์ฒ๋ฆฌํ์ต๋๋ค:
const maxBlocksPerRequest = 100
const blockChunks = []
for (let i = 0; i < allBlocks.length; i += maxBlocksPerRequest) {
blockChunks.push(allBlocks.slice(i, i + maxBlocksPerRequest))
}
// ์ฒซ ๋ฒ์งธ ์ฒญํฌ๋ ํ์ด์ง ์์ฑ ์
const response = await fetch(`https://api.notion.com/v1/pages`, {
// ... ์ฒซ ๋ฒ์งธ ์ฒญํฌ ํฌํจ
})
// ๋๋จธ์ง ์ฒญํฌ๋ append_block_children API๋ก ์ถ๊ฐ
for (let i = 1; i < blockChunks.length; i++) {
await fetch(`https://api.notion.com/v1/blocks/${pageId}/children`, {
method: 'PATCH',
// ... ๋๋จธ์ง ์ฒญํฌ ์ถ๊ฐ
})
}
๋ค์ฏ ๋ฒ์งธ ๋ฌธ์ : ์ค๋ณต ์ ๋ก๋ ๋ฐฉ์ง
๊ธฐ์กด MDX ํ์ผ์ Notion์ผ๋ก ์ ๋ก๋ํ ๋, ๊ฐ์ ์ ๋ชฉ์ ํฌ์คํธ๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ฉด ์ค๋ณต์ผ๋ก ์์ฑ๋๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ
์ ๋ก๋ ์ ์ ๊ธฐ์กด ํฌ์คํธ์ ์ ๋ชฉ์ ๋ฏธ๋ฆฌ ์กฐํํด์ ์ค๋ณต์ ์ฒดํฌํ์ต๋๋ค:
// ๊ธฐ์กด ํฌ์คํธ ์ ๋ชฉ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
const existingPagesResponse = await fetch(/* ... */)
const existingTitles = new Set()
for (const page of existingPages.results) {
const title = page.properties?.Title?.title?.[0]?.plain_text || ''
if (title) {
existingTitles.add(title.toLowerCase().trim())
}
}
// ์
๋ก๋ ์ ์ค๋ณต ์ฒดํฌ
if (existingTitles.has(titleKey)) {
console.log(`โญ๏ธ "${title}" ์ ๋ชฉ์ด ์ด๋ฏธ ์กด์ฌํ์ฌ ๊ฑด๋๋๋๋ค.`)
continue
}
์ต์ข ๊ตฌํ ๊ฒฐ๊ณผ
์๋ฐฉํฅ ๋๊ธฐํ๊ฐ ์์ฑ๋์์ต๋๋ค:
Notion โ MDX (๋ค์ด๋ก๋)
yarn sync-notion
Notion ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํฌ์คํธ๋ฅผ ๊ฐ์ ธ์์ MDX ํ์ผ๋ก ๋ณํํฉ๋๋ค. ๋น๋ ์ ์๋์ผ๋ก ์คํ๋๋๋ก ์ค์ ํ์ต๋๋ค.
MDX โ Notion (์ ๋ก๋)
yarn upload-to-notion
๊ธฐ์กด MDX ํ์ผ์ Notion ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก ์ ๋ก๋ํฉ๋๋ค. ์ค๋ณต ์ฒดํฌ์ ์ฑ๊ณต/์คํจ ํต๊ณ๋ฅผ ์ ๊ณตํฉ๋๋ค.
์ฌ์ฉ ํ
-
Integration ์ฐ๊ฒฐ ํ์ธ: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ์ ์ ๋๋ก ๊ฐ์ ธ์ค๋ ค๋ฉด Integration์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๊ฒฐ๋์ด ์์ด์ผ ํฉ๋๋ค.
-
์์ฑ๋ช ์ผ์น: ์คํฌ๋ฆฝํธ์์ ์ฌ์ฉํ๋ ์์ฑ๋ช ์ด Notion ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ค์ ์์ฑ๋ช ๊ณผ ์ ํํ ์ผ์นํด์ผ ํฉ๋๋ค.
-
๋ธ๋ก ๊ฐ์ ์ ํ: ๋งค์ฐ ๊ธด ๊ธ์ ๊ฒฝ์ฐ ์ฌ๋ฌ ๋ฒ์ API ํธ์ถ์ด ํ์ํ๋ฏ๋ก ์๊ฐ์ด ๊ฑธ๋ฆด ์ ์์ต๋๋ค.
-
์๋ฌ ์ฒ๋ฆฌ: ์ ๋ก๋ ์คํจ ์ ์์ธํ ์๋ฌ ๋ฉ์์ง๋ฅผ ํ์ธํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
๋ง๋ฌด๋ฆฌ
Notion์ ๋ธ๋ก๊ทธ์ ์ฝํ ์ธ ์์ค๋ก ์ฌ์ฉํ๋ฉด์ ๊ธ ์์ฑ์ด ํจ์ฌ ํธํด์ก์ต๋๋ค. ํนํ ์ฌ๋ฌ ๊ธฐ๊ธฐ์์ ์์ ํ๊ฑฐ๋ ํ์ ์ด ํ์ํ ๋ Notion์ ์ฅ์ ์ด ๋น์ ๋ฐํฉ๋๋ค.
๋ฌผ๋ก ๋ช ๊ฐ์ง ์ ์ฝ์ฌํญ๋ ์์์ง๋ง, ๋๋ถ๋ถ์ API์ ํน์ฑ์ ์ดํดํ๊ณ ์ ์ ํ ์ฐํํ๋ ๋ฐฉ๋ฒ์ผ๋ก ํด๊ฒฐํ ์ ์์์ต๋๋ค. Notion API๊ฐ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ผ๋, ์์ผ๋ก ๋ ๋ง์ ๊ธฐ๋ฅ์ด ์ถ๊ฐ๋ ๊ฒ์ผ๋ก ๊ธฐ๋ํฉ๋๋ค.
์ด๋ฒ ์์ ์ ํตํด ๋ฐฐ์ด ์ ์ API ๋ฌธ์๋ง์ผ๋ก๋ ์ ์ ์๋ ์ค์ ์ฌ์ฉ ์์ ๋ฌธ์ ์ ๋ค์ด ์๋ค๋ ๊ฒ์ ๋๋ค. ์ง์ ๊ตฌํํด๋ณด๋ฉด์ ๊ฒช์ ๊ฒฝํ๋ค์ด ์ด ๊ธ์ ํต์ฌ ๋ด์ฉ์ ๋๋ค.