๋ธ”๋กœ๊ทธ

๐Ÿ‘ญ ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ๋ฅผ ํ™œ์šฉํ•˜์—ฌ 1๊ฐœ ๊ฐ€๊ฒฉ์œผ๋กœ 2๊ฐœ์˜ Next.js ์›น์‚ฌ์ดํŠธ ๊ตฌ์ถ•ํ•˜๊ธฐ

Leonardo Losoviz
์ž‘์„ฑ์ž: Leonardo Losoviz ยท

์ตœ๊ทผ Gato GraphQL ํŒ€์€ Gato Plugins๋ฅผ ์ถœ์‹œํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” Gato GraphQL์˜ ํ˜•์ œ ์‚ฌ์ดํŠธ์ž…๋‹ˆ๋‹ค.

๋‘ ์‚ฌ์ดํŠธ๊ฐ€ ๋™์ผํ•œ ์‚ฌ์ดํŠธ๋ผ๋Š” ๊ฒƒ์„ ๋ˆˆ์น˜์ฑ„์‹ค ๊ฒ๋‹ˆ๋‹ค! ๋‘ ์‚ฌ์ดํŠธ์˜ ์œ ์ผํ•œ ์ฐจ์ด์ ์€ ์ƒ‰์ƒ ์ฒด๊ณ„์ž…๋‹ˆ๋‹ค. Gato GraphQL์€ ๋‹คํฌ ํ…Œ๋งˆ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , Gato Plugins๋Š” ๋ผ์ดํŠธ ํ…Œ๋งˆ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋‘ ์‚ฌ์ดํŠธ์˜ ๋ธ”๋กœ๊ทธ ์„น์…˜์€ ์™„์ „ํžˆ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

gatographql.com์˜ ๋ธ”๋กœ๊ทธ ์„น์…˜
gatographql.com์˜ ๋ธ”๋กœ๊ทธ ์„น์…˜
gatoplugins.com์˜ ๋ธ”๋กœ๊ทธ ์„น์…˜
gatoplugins.com์˜ ๋ธ”๋กœ๊ทธ ์„น์…˜

๋ฌธ์„œ ์„น์…˜๋„ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

gatographql.com์˜ Docs ์„น์…˜
gatographql.com์˜ Docs ์„น์…˜
gatoplugins.com์˜ Docs ์„น์…˜
gatoplugins.com์˜ Docs ์„น์…˜

์„น์…˜์ด ๋‹ค๋ฅผ ๋•Œ๋„ ์žˆ์ง€๋งŒ, ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ํ† ๋Œ€๋Š” ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, Gato GraphQL์˜ extensions์™€ Gato Plugins์˜ ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ๋™์ผํ•œ ๋ ˆ์ด์•„์›ƒ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

gatographql.com์˜ Extensions ์„น์…˜
gatographql.com์˜ Extensions ์„น์…˜
gatoplugins.com์˜ Plugins ์„น์…˜
gatoplugins.com์˜ Plugins ์„น์…˜

(๊ทธ๋Ÿฐ๋ฐ, ๋กœ๊ณ ๋„ ๊ฑฐ์˜ ๊ฐ™์Šต๋‹ˆ๋‹ค! ๐Ÿ˜œ)

gatographql.com์˜ ๋กœ๊ณ 
gatographql.com์˜ ๋กœ๊ณ 
gatoplugins.com์˜ ๋กœ๊ณ 
gatoplugins.com์˜ ๋กœ๊ณ 

๊ทธ๋ฆฌ๊ณ  ๋„ค, ์ด ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ๋„ ์–‘์ชฝ ์‚ฌ์ดํŠธ์— ๋ชจ๋‘ ๊ฒŒ์žฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค! ๐Ÿ˜‚

gatoplugins.com์—์„œ ์ฝ๊ธฐ: Building 2 Nextjs websites at the price of 1, by hacking the light/dark mode.

๋‹จ, ๋‘ ์‚ฌ์ดํŠธ์˜ ํฌ์ŠคํŠธ์—๋Š” ์ •ํ™•ํžˆ 7๊ฐ€์ง€ ์ฐจ์ด์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋‘ ์ฐพ์„ ์ˆ˜ ์žˆ์œผ์‹ ๊ฐ€์š”? ์ฐพ์œผ์‹ ๋‹ค๋ฉด Gato GraphQL ํ• ์ธ ์ฟ ํฐ์„ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค ๐Ÿ™

๋ผ์ดํŠธ/๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด 2๊ฐœ์˜ ์›น์‚ฌ์ดํŠธ๋ฅผ ๋งŒ๋“  ์ด์œ 

์ด์œ ๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

๋‘ ๊ฐœ์˜ ๋ณ„๋„ ์ฝ”๋“œ๋ฒ ์ด์Šค๋ฅผ ์œ ์ง€ํ•  ์‹œ๊ฐ„๋„ ์ฒด๋ ฅ๋„ ์—†์Šต๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•˜๊ฒŒ ์œ ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์›น์‚ฌ์ดํŠธ์— ์Ÿ๋Š” ํ•œ ์‹œ๊ฐ„์€ ์ œํ’ˆ ๊ฐœ๋ฐœ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ํ•œ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๋“ค์ด ๊ฐ™์€ ํŒจ๋ฐ€๋ฆฌ์˜ ์ผ์›์œผ๋กœ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋„๋ก ์œ ์‚ฌํ•˜๊ฒŒ ๋ณด์ด๊ธธ ์›ํ•ฉ๋‹ˆ๋‹ค.

์ €๋Š” ๋””์ž์ด๋„ˆ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค. ๊ทธ ๋ชจ์Šต๊ณผ ์Šคํƒ€์ผ์„ ๋‹ฌ์„ฑํ•œ ๊ฒƒ์— ๋งŒ์กฑํ–ˆ๊ณ , ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

๋‹ค์‹œ ๋งํ•ด์„œ, ์ €๋ ดํ•˜๊ณ  ๊ฐ„ํŽธํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์—„์ฒญ๋‚œ ์‹œ๊ฐ„๊ณผ ๋…ธ๋ ฅ์„ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ , ๊ทธ๊ฒƒ์„ ์ œํ’ˆ ๊ฐœ๋ฐœ์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ ์œผ๋กœ๋Š”, 2๊ฐœ์˜ ์‚ฌ์ดํŠธ๊ฐ€ ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ ์ „ํ™˜์„ ์ง€์›ํ•  ์ˆ˜ ์—†์–ด ์Šคํƒ€์ผ์ด ๊ณ ์ •๋˜์–ด ์žˆ์ง€๋งŒ, ๊ทธ๊ฒƒ์€ ๊ฐ์ˆ˜ํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.


์ž, ๊ทธ๋Ÿผ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์Šคํƒ: ์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ Next.js๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋ฉฐ, ์Šคํƒ€์ผ๋ง์—๋Š” Tailwind CSS๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Cruip์˜ ์—ฌ๋Ÿฌ ํ…œํ”Œ๋ฆฟ์„ ์กฐํ•ฉํ•˜์—ฌ ํ•„์š”์— ๋งž๊ฒŒ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜์—ฌ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. (๊ทธ ํ…œํ”Œ๋ฆฟ๋“ค์€ ์ •๋ง ์•„๋ฆ„๋‹ต์Šต๋‹ˆ๋‹ค!)

์ฝ˜ํ…์ธ ๋Š” Contentlayer๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๊ณตํ†ต ์ฝ”๋“œ๋ฅผ ๊ณต์œ  ํŒจํ‚ค์ง€๋กœ ์ถ”์ถœํ•˜๊ณ  ๋ชจ๋…ธ๋ ˆํฌ์— ํ†ตํ•ฉํ•˜๊ธฐ

๋‘ ์›น์‚ฌ์ดํŠธ์˜ ์ฝ”๋“œ๋ฒ ์ด์Šค๊ฐ€ ๋™์ผํ•˜๋ฏ€๋กœ, ๋ชจ๋…ธ๋ ˆํฌ์— ํ•จ๊ป˜ ํ˜ธ์ŠคํŒ…ํ•˜๋Š” ๊ฒƒ์ด ์ž์—ฐ์Šค๋Ÿฌ์šด ์„ ํƒ์ž…๋‹ˆ๋‹ค.

๊ธฐ์กด ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—๋Š” ๋‹จ์ผ ํ”„๋กœ์ ํŠธ๋งŒ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • gatographql.com

์ด๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์žฌ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

  • apps/gatographql.com: Gato GraphQL ์›น์‚ฌ์ดํŠธ
  • apps/gatoplugins.com: Gato Plugins ์›น์‚ฌ์ดํŠธ
  • packages/shared/gatoapp: ๋‘ ์›น์‚ฌ์ดํŠธ์˜ ๊ณตํ†ต ์ฝ”๋“œ

๋‹ค์Œ์€ VSCode์—์„œ์˜ ์ œ ์›Œํฌ์ŠคํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค.

์ œ ๋ชจ๋…ธ๋ ˆํฌ ๊ตฌ์กฐ
์ œ ๋ชจ๋…ธ๋ ˆํฌ ๊ตฌ์กฐ

๋ชจ๋…ธ๋ ˆํฌ์— ๋ณต์žกํ•œ ๋„๊ตฌ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉฐ, ๊ฐ„๋‹จํ•œ workspaces๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํžˆ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋…ธ๋ ˆํฌ ๋ฃจํŠธ์˜ package.json์€ ํ˜„์žฌ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

{
  "name": "gatowebsites",
  "version": "3.0.0",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/shared/*"
  ]
}

๋˜ํ•œ, ๋‘ ํ”„๋กœ์ ํŠธ์˜ ์‹คํ–‰/๋นŒ๋“œ/๋ฐฐํฌ๋ฅผ ์œ„ํ•œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ package.json์— ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค(Netlify์— ๋ฐฐํฌํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋„ ํฌํ•จํ•˜๋ฉฐ, ๋‘ ์‚ฌ์ดํŠธ ๋ชจ๋‘ ๊ทธ๊ณณ์— ํ˜ธ์ŠคํŒ…๋ฉ๋‹ˆ๋‹ค).

{
  "scripts": {
    "dev-gatographql": "npm run dev --workspace=apps/gatographql",
    "build-gatographql": "npm run build --workspace=apps/gatographql",
    "deploy-gatographql": "npm run deploy-staging-gatographql",
    "deploy-dev-gatographql": "netlify dev --filter gatographql",
    "deploy-staging-gatographql": "netlify deploy --build --context deploy-preview --filter gatographql",
    "deploy-prod-gatographql": "netlify deploy --build --prod --context production --filter gatographql",
 
    "dev-gatoplugins": "npm run dev --workspace=apps/gatoplugins",
    "build-gatoplugins": "npm run build --workspace=apps/gatoplugins",
    "deploy-gatoplugins": "npm run deploy-staging-gatoplugins",
    "deploy-dev-gatoplugins": "netlify dev --filter gatoplugins",
    "deploy-staging-gatoplugins": "netlify deploy --build --context deploy-preview --filter gatoplugins",
    "deploy-prod-gatoplugins": "netlify deploy --build --prod --context production --filter gatoplugins"
  }
}

์ปค์Šคํ…€ ๋ฐ์ดํ„ฐ๋ฅผ props๋กœ ๋ฐ›๋„๋ก ์ปดํฌ๋„ŒํŠธ ๋ณ€ํ™˜ํ•˜๊ธฐ

๊ฐ€๋Šฅํ•œ ํ•œ ๊ฐ ์›น์‚ฌ์ดํŠธ์˜ ์ฝ”๋“œ๋ฅผ ๊ณต์œ  ํŒจํ‚ค์ง€๋กœ ์˜ฎ๊ธฐ๊ณ , props๋ฅผ ํ†ตํ•ด ๋™์ž‘์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ๊ณต์œ  ํŒจํ‚ค์ง€ gatoapp์—๋Š” ๋‘ ์‚ฌ์ดํŠธ์˜ /blog ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•˜๊ธฐ ์œ„ํ•œ BlogSection ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

import PopularPosts from 'gatoapp/components/blog/popular-posts'
import PageHeader from 'gatoapp/components/page-header'
import { BlogPostProps } from 'gatoapp/types/list-types'
import BlogSectionPostList from './blog-section-post-list'
import { useEffect, useState, Suspense } from "react";
 
export default function BlogSection({
  blogPosts,
  title = "Blog",
  description,
  campaignBanner,
}: {
  blogPosts: BlogPostProps[],
  title?: string,
  description: string,
  campaignBanner?: React.ReactNode
}) {
  const sidebar = (
    <aside className="hidden sm:block relative mt-12 md:mt-0 md:w-64 md:ml-12 lg:ml-20 md:shrink-0">
      <PopularPosts
        blogPosts={blogPosts}
      />
    </aside>
  )
 
  return (
    <div className="max-w-6xl mx-auto px-4 sm:px-6">
      <div className="pt-32 pb-12 md:pt-40 md:pb-20">
 
        {campaignBanner}
 
        {/* Page header */}
        <PageHeader
          title={title}
          description={description}
        />
 
        {/* Main content */}
        <BlogSectionPostList
          blogPosts={blogPosts}
          sidebar={sidebar}
        />
 
      </div>
    </div>
  )
}

๋ชจ๋“  ์ฝ˜ํ…์ธ ๋Š” ๋™์ผํ•˜์ง€๋งŒ, ๋‹ค์Œ ํ•ญ๋ชฉ์€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

  • ํŽ˜์ด์ง€ ํ—ค๋” (์ œ๋ชฉ/์„ค๋ช…)
  • ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ
  • ์บ ํŽ˜์ธ ๋ฐฐ๋„ˆ

๋‘ ์›น์‚ฌ์ดํŠธ๊ฐ€ ์„œ๋กœ ๋…๋ฆฝ์ ์œผ๋กœ ์บ ํŽ˜์ธ์„ ์šด์˜ํ•  ์ˆ˜ ์žˆ๋„๋ก, campaignBanner๋ฅผ React.ReactNode๋กœ ์ „๋‹ฌํ•˜๋ฉด ์บ ํŽ˜์ธ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์— ์ œ์•ฝ์ด ์—†์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์ด ๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ๋ฅผ ๊ฒŒ์‹œํ•˜๋Š” ์‹œ์ ์—๋Š” Gato GraphQL์—์„œ ์บ ํŽ˜์ธ์„ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์ง€๋งŒ, Gato Plugins์—์„œ๋Š” ์ง„ํ–‰ํ•˜์ง€ ์•Š๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

gatographql.com์˜ ์บ ํŽ˜์ธ ๋ฐฐ๋„ˆ
gatographql.com์˜ ์บ ํŽ˜์ธ ๋ฐฐ๋„ˆ

๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ๋ฅผ ์ฃผ์ž…ํ•˜๋ ค๋ฉด ์ข€ ๋” ๋งŽ์€ ๋กœ์ง์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ ์ฃผ์ž…ํ•˜๊ธฐ

๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋Š” blogPosts prop์„ ํ†ตํ•ด BlogSection์— ์ฃผ์ž…๋ฉ๋‹ˆ๋‹ค.

Contentlayer๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๊ฐ ์›น์‚ฌ์ดํŠธ๋Š” ๋ฃจํŠธ์— contentlayer.config.js ํŒŒ์ผ์„ ๊ฐ€์ง€๋ฉฐ, ์‚ฌ์ดํŠธ์˜ ํƒ€์ž…์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

์ด ์„ค์ • ํŒŒ์ผ์€ ๊ณต์œ  ํŒจํ‚ค์ง€ gatoapp์œผ๋กœ ์ด๋™ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ณต์œ  ํƒ€์ž…์˜ ์„ค์ •์„ ์ œ๊ณตํ•˜๋Š” ๋‚ด๋ณด๋‚ด๊ธฐ ๋ชจ๋“ˆ์„ ๋งŒ๋“ค๊ณ , ๊ฐ ์‚ฌ์ดํŠธ์˜ contentlayer.config.js์—์„œ ์ด๋ฅผ ์ž„ํฌํŠธํ•˜์—ฌ ๋กœ์ง์„ DRYํ•˜๊ฒŒ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

gatoapp์—๋Š” ๊ณต์œ  ํƒ€์ž… BlogPost๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋‚ด๋ณด๋‚ด๊ธฐ ๋ชจ๋“ˆ contentlayer.config.js๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

import { defineDocumentType } from 'contentlayer2/source-files'
 
const BlogPost = defineDocumentType(() => ({
  name: 'BlogPost',
  filePathPattern: `blog/**/*.mdx`,
  contentType: 'mdx',
  fields: {
    title: {
      type: 'string',
      required: true
    },
    publishedAt: {
      type: 'date',
      required: true
    },
    description: {
      type: 'string',
      required: true,
    },
    image: {
      type: 'string',
    },
  },
  computedFields: {
    slug: {
      type: 'string',
      resolve: (doc) => doc._raw.flattenedPath.replace(new RegExp('^blog/?'), ''),
    },
    urlPath: {
      type: 'string',
      resolve: (doc) => `/blog/${doc._raw.flattenedPath.replace(new RegExp('^blog/?'), '')}`,
    },
  },
}))
 
module.exports = {
  types: {
    BlogPost: BlogPost,
  },
}

apps/gatographql.com๊ณผ apps/gatoplugins.com ๋ชจ๋‘์˜ contentlayer.config.js ํŒŒ์ผ์—์„œ ํ•ด๋‹น ํƒ€์ž…์„ ์ž„ํฌํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { makeSource } from 'contentlayer2/source-files'
import ContentLayerConfig from '../../packages/shared/gatoapp/contentlayer.config.js'
 
const BlogPost = ContentLayerConfig.types.BlogPost
 
export default makeSource({
  documentTypes: [BlogPost],
})

์ผ๋ฐ˜์ ์œผ๋กœ ์ฝ”๋“œ์—์„œ ํƒ€์ž… BlogPost๋ฅผ ์ฐธ์กฐํ•˜๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž„ํฌํŠธํ•ฉ๋‹ˆ๋‹ค.

import { BlogPost } from '@/.contentlayer/generated'

๊ทธ๋Ÿฌ๋‚˜ ํƒ€์ž… BlogPost๋Š” ๊ณต์œ  ํŒจํ‚ค์ง€๊ฐ€ ์•„๋‹Œ ์›น์‚ฌ์ดํŠธ ์•„๋ž˜์— ์กด์žฌํ•˜๋ฏ€๋กœ, ๊ณต์œ  ์ฝ”๋“œ์—์„œ ํ•ด๋‹น ํƒ€์ž…์„ ์ง์ ‘ ์ฐธ์กฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ํ•ดํ‚น์œผ๋กœ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ปดํŒŒ์ผ๋œ Contentlayer ํŒŒ์ผ(apps/gatographql/.contentlayer/generated/types.d.ts ์•„๋ž˜)์—์„œ ํ•ด๋‹น ํƒ€์ž…์˜ ์ •์˜๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ, ๊ณต์œ  ํŒจํ‚ค์ง€์˜ ์ƒˆ types.tsx ํŒŒ์ผ์— ๋ถ™์—ฌ๋„ฃ์Šต๋‹ˆ๋‹ค.

import type { MDX, IsoDateTimeString } from 'contentlayer2/core'
 
export type BlogPost = {
  // _id: string // not needed
  // _raw: Local.RawDocumentData // not needed
  type: 'BlogPost'
  title: string
  publishedAt: IsoDateTimeString
  description: string
  image?: string | undefined
  body: MDX
  slug: string,
  urlPath: string,
}

๊ทธ๋ฆฌ๊ณ  ๊ณต์œ  ์ฝ”๋“œ์—์„œ ์ด ๊ณต์œ  ํƒ€์ž…์„ ์ฐธ์กฐํ•ฉ๋‹ˆ๋‹ค.

import { BlogPost } from 'gatoapp/types'

์›น์‚ฌ์ดํŠธ์™€ ๊ณต์œ  ํŒจํ‚ค์ง€์˜ BlogPost ํƒ€์ž… ๊ฐ„์˜ ์†์„ฑ์ด ๋™์ผํ•˜๋ฏ€๋กœ, ์ „์ž๋ฅผ ํ›„์ž๋ฅผ ๊ธฐ๋Œ€ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธ€๋กœ๋ฒŒ props๋ฅผ ์ฃผ์ž…ํ•˜๊ธฐ ์œ„ํ•œ ์ปจํ…์ŠคํŠธ ๋งŒ๋“ค๊ธฐ

๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฉ”๋‰ด ์ปดํฌ๋„ŒํŠธ๋Š” ๊ณต์œ  ์ฝ”๋“œ์—์„œ ๋ Œ๋”๋ง๋˜์ง€๋งŒ, ๊ฐ ์›น์‚ฌ์ดํŠธ๊ฐ€ ์ž์ฒด ๋ฉ”๋‰ด๋ฅผ ๊ฐ€์ง€๋ฏ€๋กœ ์›น์‚ฌ์ดํŠธ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ฉ”๋‰ด๋Š” ๋ชจ๋“  ํŽ˜์ด์ง€์— ํ‘œ์‹œ๋˜๋ฏ€๋กœ, ๋งค๋ฒˆ props๋กœ ์ „๋‹ฌํ•˜๊ณ  ์‹ถ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ React ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฉ”๋‰ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ•œ ๋ฒˆ๋งŒ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

๊ณต์œ  ํŒจํ‚ค์ง€์— AppComponent๋ผ๋Š” ์ปจํ…์ŠคํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

'use client'
 
import React from 'react'
import { createContext, useContext } from 'react'
import { StaticImageData } from 'next/image'
 
type ContextProps = {
  header: {
    menu: React.ReactNode,
    mobileMenu: React.ReactNode,
  },
}
 
const AppComponentContext = createContext<ContextProps>({
  header: {
    menu: <div></div>,
    mobileMenu: <div></div>,
  },
})
 
export interface AppComponentProviderInterface extends ContextProps {
  children: React.ReactNode,
}
 
export default function AppComponentProvider({
  children,
  header,
}: AppComponentProviderInterface) {  
  return (
    <AppComponentContext.Provider value={{ header }}>
      {children}
    </AppComponentContext.Provider>
  )
}
 
export const useAppComponentProvider = () => useContext(AppComponentContext)

๊ณต์œ  ํŒจํ‚ค์ง€์—์„œ ์ด๋ฅผ ์ฐธ์กฐํ•ฉ๋‹ˆ๋‹ค.

'use client'
 
import Logo from './logo'
import HeaderMobile from './header-mobile'
import { useAppComponentProvider } from 'gatoapp/app/appcomponent-provider'
 
export default function Header() {
  const AppComponent = useAppComponentProvider()
  return (
    <header className="fixed w-full z-50">
      <div className={`absolute inset-0 bg-opacity-70 backdrop-blur -z-10 bg-white border-slate-200 border-b dark:border-b-0 dark:bg-transparent dark:border-slate-800`} aria-hidden="true"/>
      <div className="max-w-6xl mx-auto px-4 sm:px-6">
        <div className="flex items-center justify-between h-16">
 
          {/* Site branding */}
          <div className="flex-1">
            <Logo />
          </div>
 
          <nav className="hidden md:flex md:grow">
            {/* Desktop menu links */}
            {AppComponent.header.menu}
          </nav>
 
          <HeaderMobile />
 
        </div>
      </div>
    </header>
  )
}

๊ทธ๋ฆฌ๊ณ  apps/gatographql/app/(default)/layout.tsx์˜ ์›น์‚ฌ์ดํŠธ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.

import AppComponentProvider from 'gatoapp/app/appcomponent-provider'
import HeaderMenu from '@/components/menu/header-menu'
import HeaderMobileMenu from '@/components/menu/header-mobile-menu'
import DefaultLayout from 'gatoapp/app/(default)/layout'
 
export default function AppDefaultLayout({
  children,
}: {
  children: React.ReactNode
}) {  
  return (
    <AppComponentProvider
      header={{
        menu: <HeaderMenu />,
        mobileMenu: <HeaderMobileMenu />,
      }}
    >
      <DefaultLayout>
        {children}
      </DefaultLayout>
    </AppComponentProvider>
  )
}

๋งˆ์ง€๋ง‰์œผ๋กœ, ์›น์‚ฌ์ดํŠธ๊ฐ€ ์ž์ฒด HeaderMenu ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

import Link from 'next/link'
import Dropdown from 'gatoapp/components/utils/dropdown'
 
export default function HeaderMenu() {
  return (
    <ul className="flex grow justify-center flex-wrap items-center">
      <li>
        <Link href="/pricing">Pricing</Link>
      </li>
      <li>
        <Link href='/extensions'>Extensions</Link>
      </li>      
      <Dropdown title="Product">
        <li>
          <Link href='/features'>Features</Link>
        </li>
        <li>
          <Link href='/highlights'>Highlights</Link>
        </li>
        <li>
          <Link href='/demos'>Demos</Link>
        </li>
        <li>
          <Link href='/comparisons'>Comparisons</Link>
        </li>
      </Dropdown>
    </ul>
  )
}

๋ผ์ดํŠธ ๋ชจ๋“œ์™€ ๋‹คํฌ ๋ชจ๋“œ์˜ ์Šคํƒ€์ผ

Tailwind์—์„œ๋Š” ๋‹คํฌ ๋ชจ๋“œ๊ฐ€ ํ™œ์„ฑํ™”๋  ๋•Œ ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด dark: ์ ‘๋‘์‚ฌ๋ฅผ ๋ถ™์ž…๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ๊ณต์œ  ํŒจํ‚ค์ง€ ์ฝ”๋“œ์—๋Š” ๋ผ์ดํŠธ์™€ ๋‹คํฌ ๋‘ ๋ณ€ํ˜•์˜ ์Šคํƒ€์ผ์ด ๋ชจ๋‘ ํฌํ•จ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์ปดํฌ๋„ŒํŠธ PageHeader๋Š” ๋ผ์ดํŠธ ๋ชจ๋“œ(text-gray-600)์™€ ๋‹คํฌ ๋ชจ๋“œ(dark:text-slate-400)์—์„œ ์„œ๋กœ ๋‹ค๋ฅธ ์ƒ‰์ƒ์œผ๋กœ ์„ค๋ช…์„ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.

export default function PageHeader({
  title,
  description,
  children,
}: {
  title: string,
  description?: string,
  children?: React.ReactNode,
}) {
  return (
    <div className="max-w-3xl mx-auto text-center">
      <h1 className="h1 pb-4">{title}</h1>
      {description && (
        <div className="max-w-3xl mx-auto">
          <p className="text-gray-600 dark:text-slate-400">{description}</p>
        </div>
      )}
      {children}
    </div>
  )
}

์‚ฌ์ดํŠธ์— ๋ผ์ดํŠธ ๋˜๋Š” ๋‹คํฌ ๋ชจ๋“œ ์„ค์ •ํ•˜๊ธฐ

gatographql.com์€ ๋‹คํฌ ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. apps/gatographql/app/layout.tsx ํŒŒ์ผ์˜ <body>์— ํด๋ž˜์Šค๋ช… dark๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค(์Šคํƒ€์ผ๋ง์šฉ ํด๋ž˜์Šค๋ช… bg-slate-900 text-slate-100๋„ ํฌํ•จ).

import { Inter } from 'next/font/google'
import RootLayoutHeader from 'gatoapp/app/layout-header'
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap'
})
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <RootLayoutHeader />
      <body className={`${inter.variable} dark bg-slate-900 text-slate-100`}>
        {children}
      </body>
    </html>
  )
}

gatoplugins.com์€ ๋ผ์ดํŠธ ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ๊ธฐ๋ณธ ๋ชจ๋“œ์ด๋ฏ€๋กœ, <body>์— ํŠน๋ณ„ํ•œ ํด๋ž˜์Šค๋ช…์„ ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค(์Šคํƒ€์ผ๋ง์šฉ ํด๋ž˜์Šค๋ช… bg-white text-slate-800๋งŒ ์žˆ์œผ๋ฉด ๋ฉ๋‹ˆ๋‹ค).

import { Inter } from 'next/font/google'
import RootLayoutHeader from 'gatoapp/app/layout-header'
 
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap'
})
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <RootLayoutHeader />
      <body className={`${inter.variable} bg-white text-slate-800`}>
        {children}
      </body>
    </html>
  )
}

์ด์ƒ์ž…๋‹ˆ๋‹ค

์ด์ œ 1๊ฐœ ๊ฐ€๊ฒฉ์œผ๋กœ 2๊ฐœ์˜ ์›น์‚ฌ์ดํŠธ๋ฅผ ๊ฐ–๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ๊ฒƒ์— ๋งค์šฐ ๋งŒ์กฑํ•ฉ๋‹ˆ๋‹ค.

์ž, 7๊ฐ€์ง€ ์ฐจ์ด์ ์„ ์ฐพ์•„๋ณด์„ธ์š”, ๊ทธ๋ฆฌ๊ณ  ์ƒํ’ˆ์„ ๋ฐ›์œผ์„ธ์š”! ๐Ÿ˜…


๋‰ด์Šค๋ ˆํ„ฐ ๊ตฌ๋…ํ•˜๊ธฐ

Gato GraphQL์˜ ๋ชจ๋“  ์—…๋ฐ์ดํŠธ๋ฅผ ๋†“์น˜์ง€ ๋งˆ์„ธ์š”.