๐ญ ๋ผ์ดํธ/๋คํฌ ๋ชจ๋๋ฅผ ํ์ฉํ์ฌ 1๊ฐ์ ๊ฐ๊ฒฉ์ผ๋ก 2๊ฐ์ Next.js ์น์ฌ์ดํธ ๊ตฌ์ถํ๊ธฐ
์ต๊ทผ Gato GraphQL ํ์ด Gato GraphQL์ ํ์ ์ฌ์ดํธ์ธ Gato Plugins๋ฅผ ์ถ์ํ์์ต๋๋ค.
๋ ์ฌ์ดํธ๊ฐ ์์ ํ ๋์ผํ ์ฌ์ดํธ๋ผ๋ ๊ฒ์ ๋์น์ฑ์ จ์ ๊ฒ์ ๋๋ค! ์ ์ผํ ์ฐจ์ด์ ์ ์ ๊ตฌ์ฑํ์ ๋๋ค. Gato GraphQL์ ๋คํฌ ํ ๋ง๋ฅผ ์ฌ์ฉํ๊ณ , Gato Plugins๋ ๋ผ์ดํธ ํ ๋ง๋ฅผ ์ฌ์ฉํฉ๋๋ค.
๋ ์ฌ์ดํธ์ ๋ธ๋ก๊ทธ ์น์ ์ ์์ ํ ๋์ผํฉ๋๋ค:


๋ฌธ์ ์น์ ๋ ๋์ผํฉ๋๋ค:


์น์ ๋ด์ฉ์ด ๋ค๋ฅธ ๊ฒฝ์ฐ๋ ์์ง๋ง, ๊ทผ๋ณธ์ ์ธ ๊ธฐ๋ฐ์ ๋์ผํฉ๋๋ค.
์๋ฅผ ๋ค์ด, Gato GraphQL์ ํ์ฅ ๊ธฐ๋ฅ๊ณผ Gato Plugins์ ํ๋ฌ๊ทธ์ธ์ ๋์ผํ ๋ ์ด์์์ ์ฌ์ฉํฉ๋๋ค:


(์ฐธ๊ณ ๋ก, ๋ก๊ณ ๋ ๊ฑฐ์ ๊ฐ์ต๋๋ค! ๐)


๊ทธ๋ฆฌ๊ณ ๋ค, ์ด ๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ๋ ๋ ์ฌ์ดํธ ๋ชจ๋์ ๊ฒ์ฌ๋์ด ์์ต๋๋ค! ๐
gatographql.com์์๋ ์ฝ์ ์ ์์ต๋๋ค: Building 2 Nextjs websites at the price of 1, by hacking the dark/light mode
๊ทธ๋ฐ๋ฐ ๋ ์ฌ์ดํธ์ ๊ฒ์๋ฌผ์๋ ์ ํํ 7๊ฐ์ง ์ฐจ์ด์ ์ด ์์ต๋๋ค. ๋ชจ๋ ์ฐพ์ ์ ์์ผ์ ๊ฐ์? ๋ชจ๋ ์ฐพ์ผ์ ๋ถ๊ป๋ Gato GraphQL ํ ์ธ ์ฟ ํฐ์ ๋๋ฆฝ๋๋ค ๐
์ ๋ผ์ดํธ/๋คํฌ ๋ชจ๋๋ฅผ ์ฌ์ฉํด 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": "2.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์๋ BlogSection ์ปดํฌ๋ํธ๊ฐ ํฌํจ๋์ด ์์ต๋๋ค (๋ ์ฌ์ดํธ์ /blog ํ์ด์ง๋ฅผ ์ถ๋ ฅํ๊ธฐ ์ํ ๊ฒ):
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 = "Our 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์์๋ ์งํํ์ง ์์ต๋๋ค:

๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ์ ์ฃผ์ ํ๋ ค๋ฉด ์ข ๋ ๋ณต์กํ ๋ก์ง์ด ํ์ํฉ๋๋ค.
๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ ์ฃผ์ ํ๊ธฐ
๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ ๋ฐ์ดํฐ๋ 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๋ ์น์ฌ์ดํธ ์ธก์ ์กด์ฌํ๊ณ ๊ณต์ ํจํค์ง์๋ ์์ผ๋ฏ๋ก, ๊ณต์ ์ฝ๋์์ ๊ทธ ํ์
์ ์ง์ ์ฐธ์กฐํ ์ ์์ต๋๋ค.
์ด๋ฅผ ํต(hack)์ผ๋ก ํด๊ฒฐํฉ๋๋ค. ์ปดํ์ผ๋ 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>
<li>
<Link href='/roadmap'>Roadmap</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-700๋ง):
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-700`}>
{children}
</body>
</html>
)
}๋ง๋ฌด๋ฆฌ
์ด์ 1๊ฐ์ ๊ฐ๊ฒฉ์ผ๋ก 2๊ฐ์ ์น์ฌ์ดํธ๋ฅผ ๊ฐ๊ฒ ๋์์ต๋๋ค. ์ ๋ง ๋ง์กฑ์ค๋ฝ์ต๋๋ค.
์, 7๊ฐ์ง ์ฐจ์ด์ ์ ์ฐพ์ ์ํ์ ๋ฐ์ ๊ฐ์ธ์! ๐