5 min read

Next.js ๋ธ”๋กœ๊ทธ์— Notion ์—ฐ๋™ํ•˜๊ธฐ: ๋งˆํฌ๋‹ค์šด์—์„œ Notion์œผ๋กœ์˜ ์ „ํ™˜

Table of Contents

Next.js ๋ธ”๋กœ๊ทธ์— Notion ์—ฐ๋™ํ•˜๊ธฐ

๋ธ”๋กœ๊ทธ๋ฅผ ์šด์˜ํ•˜๋‹ค ๋ณด๋ฉด ๊ธ€ ์ž‘์„ฑ์„ ๋” ํŽธํ•˜๊ฒŒ ํ•˜๊ณ  ์‹ถ์€ ๋งˆ์Œ์ด ์ƒ๊น๋‹ˆ๋‹ค. ๊ธฐ์กด์—๋Š” ๋กœ์ปฌ์—์„œ ๋งˆํฌ๋‹ค์šด ํŒŒ์ผ์„ ์ž‘์„ฑํ•˜๊ณ  Git์— ์ปค๋ฐ‹ํ•˜๋Š” ๋ฐฉ์‹์ด์—ˆ๋Š”๋ฐ, Notion์˜ ํŽธ๋ฆฌํ•œ ์—๋””ํ„ฐ๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์‹ถ์–ด์กŒ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” Next.js ๋ธ”๋กœ๊ทธ์— Notion์„ ์—ฐ๋™ํ•˜๋ฉด์„œ ๊ฒช์€ ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.

์™œ Notion์„ ์„ ํƒํ–ˆ๋‚˜

๊ธฐ์กด ๋ธ”๋กœ๊ทธ๋Š” MDX ํŒŒ์ผ์„ data/blog/ ํด๋”์— ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์˜ ์žฅ์ ์€ ๋ช…ํ™•ํ•˜์ง€๋งŒ, ๋ช‡ ๊ฐ€์ง€ ๋ถˆํŽธํ•œ ์ ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค:

  • ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ๋งŒ ๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Œ
  • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ๊ฐ€ ๋ฒˆ๊ฑฐ๋กœ์›€
  • ์—๋””ํ„ฐ ๊ธฐ๋Šฅ์ด ์ œํ•œ์ 
  • ์—ฌ๋Ÿฌ ๊ธฐ๊ธฐ์—์„œ ์ž‘์—…ํ•˜๊ธฐ ์–ด๋ ค์›€

Notion์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฐ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ Notion์˜ ๊ฐ•๋ ฅํ•œ ์—๋””ํ„ฐ์™€ ํ˜‘์—… ๊ธฐ๋Šฅ์€ ๋ธ”๋กœ๊ทธ ์šด์˜์„ ํ›จ์”ฌ ํŽธํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

๊ตฌํ˜„ ๊ณ„ํš

Notion ์—ฐ๋™์„ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. Notion โ†’ MDX ๋™๊ธฐํ™”: Notion์—์„œ ์ž‘์„ฑํ•œ ๊ธ€์„ MDX ํŒŒ์ผ๋กœ ๋ณ€ํ™˜
  2. MDX โ†’ Notion ์—…๋กœ๋“œ: ๊ธฐ์กด MDX ํŒŒ์ผ์„ Notion์œผ๋กœ ์—…๋กœ๋“œ
  3. ์ž๋™ ๋นŒ๋“œ ํ†ตํ•ฉ: ๋นŒ๋“œ ์‹œ ์ž๋™์œผ๋กœ ์ตœ์‹  ๊ธ€ ๋™๊ธฐํ™”

Notion API ์„ค์ •

๋จผ์ € Notion Integration์„ ์ƒ์„ฑํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Integration ์ƒ์„ฑ

  1. Notion Integrations ํŽ˜์ด์ง€์—์„œ ์ƒˆ Integration ์ƒ์„ฑ
  2. Internal Integration Token ๋ณต์‚ฌ
  3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— 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 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ์—…๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. ์ค‘๋ณต ์ฒดํฌ์™€ ์„ฑ๊ณต/์‹คํŒจ ํ†ต๊ณ„๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ ํŒ

  1. Integration ์—ฐ๊ฒฐ ํ™•์ธ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์†์„ฑ์„ ์ œ๋Œ€๋กœ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด Integration์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  2. ์†์„ฑ๋ช… ์ผ์น˜: ์Šคํฌ๋ฆฝํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์†์„ฑ๋ช…์ด Notion ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์‹ค์ œ ์†์„ฑ๋ช…๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  3. ๋ธ”๋ก ๊ฐœ์ˆ˜ ์ œํ•œ: ๋งค์šฐ ๊ธด ๊ธ€์˜ ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ ๋ฒˆ์˜ API ํ˜ธ์ถœ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  4. ์—๋Ÿฌ ์ฒ˜๋ฆฌ: ์—…๋กœ๋“œ ์‹คํŒจ ์‹œ ์ƒ์„ธํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ํ™•์ธํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

Notion์„ ๋ธ”๋กœ๊ทธ์˜ ์ฝ˜ํ…์ธ  ์†Œ์Šค๋กœ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๊ธ€ ์ž‘์„ฑ์ด ํ›จ์”ฌ ํŽธํ•ด์กŒ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์—ฌ๋Ÿฌ ๊ธฐ๊ธฐ์—์„œ ์ž‘์—…ํ•˜๊ฑฐ๋‚˜ ํ˜‘์—…์ด ํ•„์š”ํ•  ๋•Œ Notion์˜ ์žฅ์ ์ด ๋น›์„ ๋ฐœํ•ฉ๋‹ˆ๋‹ค.

๋ฌผ๋ก  ๋ช‡ ๊ฐ€์ง€ ์ œ์•ฝ์‚ฌํ•ญ๋„ ์žˆ์—ˆ์ง€๋งŒ, ๋Œ€๋ถ€๋ถ„์€ API์˜ ํŠน์„ฑ์„ ์ดํ•ดํ•˜๊ณ  ์ ์ ˆํžˆ ์šฐํšŒํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. Notion API๊ฐ€ ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์œผ๋‹ˆ, ์•ž์œผ๋กœ ๋” ๋งŽ์€ ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋  ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฒˆ ์ž‘์—…์„ ํ†ตํ•ด ๋ฐฐ์šด ์ ์€ API ๋ฌธ์„œ๋งŒ์œผ๋กœ๋Š” ์•Œ ์ˆ˜ ์—†๋Š” ์‹ค์ œ ์‚ฌ์šฉ ์‹œ์˜ ๋ฌธ์ œ์ ๋“ค์ด ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด๋ฉด์„œ ๊ฒช์€ ๊ฒฝํ—˜๋“ค์ด ์ด ๊ธ€์˜ ํ•ต์‹ฌ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.