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


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


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


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


๊ทธ๋ฆฌ๊ณ ๋ค, ์ด ๋ธ๋ก๊ทธ ํฌ์คํธ๋ ์์ชฝ ์ฌ์ดํธ์ ๋ชจ๋ ๊ฒ์ฌ๋์ด ์์ต๋๋ค! ๐
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์์๋ ์งํํ์ง ์๊ณ ์์ต๋๋ค.

๋ธ๋ก๊ทธ ํฌ์คํธ๋ฅผ ์ฃผ์ ํ๋ ค๋ฉด ์ข ๋ ๋ง์ ๋ก์ง์ด ํ์ํฉ๋๋ค.
๋ธ๋ก๊ทธ ํฌ์คํธ ์ฃผ์ ํ๊ธฐ
๋ธ๋ก๊ทธ ํฌ์คํธ ๋ฐ์ดํฐ๋ 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๊ฐ์ง ์ฐจ์ด์ ์ ์ฐพ์๋ณด์ธ์, ๊ทธ๋ฆฌ๊ณ ์ํ์ ๋ฐ์ผ์ธ์! ๐