Ver3.0 ๐ ํค๋๋ฆฌ์ค CMS์ JAMstack์ผ๋ก ์์ํ๋ ์ฝํ ์ธ ๊ด๋ฆฌ ํ๋ช - ํ๋์ ์น ๊ฐ๋ฐ์ ์๋ก์ด ํจ๋ฌ๋ค์

๐ ํค๋๋ฆฌ์ค CMS์ JAMstack์ผ๋ก ์์ํ๋ ์ฝํ ์ธ ๊ด๋ฆฌ ํ๋ช - ํ๋์ ์น ๊ฐ๋ฐ์ ์๋ก์ด ํจ๋ฌ๋ค์
์ ํต์ ์ธ ์น ๊ฐ๋ฐ ๋ฐฉ์์ ๋ฐ์ด๋๋ ํ์ ์ ์ธ ์ํคํ ์ฒ ์๋ฒฝ ๊ฐ์ด๋
ํค๋๋ฆฌ์ค CMS = ์ฝํ ์ธ ์ ์ฅ์ + API
ํ๋ก ํธ์๋๋ ๋๊ฐ ์ํ๋ ๋๋ก ์์ ๋กญ๊ฒ ๊ตฌ์ถ!
// Contentful API ํธ์ถ ์์ (JavaScript)
const client = contentful.createClient({
space: 'your_space_id',
accessToken: 'your_access_token'
})
// ๋ธ๋ก๊ทธ ํฌ์คํธ ๊ฐ์ ธ์ค๊ธฐ
client.getEntries({
content_type: 'blogPost',
order: '-sys.createdAt',
limit: 10
})
.then(response => {
console.log(response.items)
})
"์๋ฒ์์ HTML์ ์์ฑํ์ง ๋ง๋ผ. ๋น๋ ํ์์ ๋ฏธ๋ฆฌ ๋ง๋ค์ด๋ผ!"
// Next.js ํ๋ก์ ํธ ์์ํ๊ธฐ
npx create-next-app@latest my-jamstack-site
cd my-jamstack-site
npm run dev
// ์ ์ ์ฌ์ดํธ ๋น๋
npm run build
npm run export
**Step 2: ํค๋๋ฆฌ์ค CMS ์ฐ๊ฒฐ** ๐
// Strapi API์์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ (Next.js ์์)
export async function getStaticProps() {
const res = await fetch('https://your-strapi.com/api/posts')
const posts = await res.json()
return {
props: {
posts
},
revalidate: 60 // ISR: 60์ด๋ง๋ค ์ฌ์์ฑ
}
}
**Step 3: Git์ ํธ์** ๐ค
```
git add .
git commit -m "Add new blog post"
git push origin main
```
**Step 4: ์๋ ๋ฐฐํฌ** ๐
Netlify๋ Vercel์ ์ฐ๊ฒฐํด๋๋ฉด, Git์ ํธ์ํ๋ ์๊ฐ ์๋์ผ๋ก:
- ๋น๋๊ฐ ์์๋ผ
- ํ
์คํธ๊ฐ ์คํ๋ผ
- CDN์ ๋ฐฐํฌ๋ผ
- ์ ์ธ๊ณ ์ฌ์ฉ์๊ฐ ์ ๊ทผ ๊ฐ๋ฅํด์ ธ!
๋ชจ๋ ๊ฒ **5๋ถ ์์** ๋๋! ๐
npx create-next-app@latest my-blog
cd my-blog
npm install contentful
**2๋จ๊ณ: Contentful ์ค์ **
Contentful ๋์๋ณด๋์์:
- Space ์์ฑ
- Content Model ์ ์ (Blog Post)
- ํ๋ ์ถ๊ฐ: title, slug, content, publishDate, author, featuredImage
**3๋จ๊ณ: ํ๊ฒฝ ๋ณ์ ์ค์ **
`.env.local` ํ์ผ ์์ฑ:
CONTENTFUL_SPACE_ID=your_space_id
CONTENTFUL_ACCESS_TOKEN=your_access_token
CONTENTFUL_PREVIEW_ACCESS_TOKEN=your_preview_token
**4๋จ๊ณ: Contentful ํด๋ผ์ด์ธํธ ์์ฑ**
`lib/contentful.js`:
import { createClient } from 'contentful'
const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
})
export async function getAllPosts() {
const entries = await client.getEntries({
content_type: 'blogPost',
order: '-fields.publishDate',
})
return entries.items.map(item => ({
title: item.fields.title,
slug: item.fields.slug,
content: item.fields.content,
publishDate: item.fields.publishDate,
author: item.fields.author,
featuredImage: item.fields.featuredImage?.fields.file.url,
}))
}
export async function getPostBySlug(slug) {
const entries = await client.getEntries({
content_type: 'blogPost',
'fields.slug': slug,
limit: 1,
})
return entries.items[0]
}
**5๋จ๊ณ: ํ์ด์ง ์์ฑ**
`pages/blog/[slug].js`:
import { getAllPosts, getPostBySlug } from '../../lib/contentful'
import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.fields.title}</h1>
<time>{new Date(post.fields.publishDate).toLocaleDateString()}</time>
<img src={post.fields.featuredImage?.fields.file.url} alt={post.fields.title} />
<div>
{documentToReactComponents(post.fields.content)}
</div>
</article>
)
}
export async function getStaticPaths() {
const posts = await getAllPosts()
return {
paths: posts.map(post => ({
params: { slug: post.slug }
})),
fallback: 'blocking'
}
}
export async function getStaticProps({ params }) {
const post = await getPostBySlug(params.slug)
return {
props: { post },
revalidate: 60 // ISR: 60์ด๋ง๋ค ์ฌ๊ฒ์ฆ
}
}
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 10 // 10์ด๋ง๋ค ์ฌ๊ฒ์ฆ
}
}
์ด๋ ๊ฒ ํ๋ฉด:
- โ
๋น๋ ์๊ฐ์ด ์งง์์ ธ (๋ชจ๋ ํ์ด์ง๋ฅผ ๋ฏธ๋ฆฌ ๋ง๋ค ํ์ ์์)
- โ
์ฝํ
์ธ ๊ฐ ํญ์ ์ต์ ์ผ๋ก ์ ์ง๋ผ
- โ
์ฑ๋ฅ์ ์ฌ์ ํ ์ด๊ณ ์!
### ๐จ ๋ค์ํ ํ๋ ์์ํฌ ์กฐํฉ
Next.js + Contentful๋ง์ด ๋ต์ ์๋์ผ! ๋ค๋ฅธ ๋ฉ์ง ์กฐํฉ๋ค๋ ์ดํด๋ณด์:
**์กฐํฉ 1: Gatsby + Strapi** ๐ฏ
- ์คํ์์ค ์กฐํฉ์ผ๋ก ๋น์ฉ ์ ๊ฐ
- Gatsby์ ๊ฐ๋ ฅํ ํ๋ฌ๊ทธ์ธ ์ํ๊ณ
- Strapi์ ์ ์ฐํ ์ปค์คํฐ๋ง์ด์ง
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: 'gatsby-source-strapi',
options: {
apiURL: 'http://localhost:1337',
contentTypes: ['article', 'user'],
queryLimit: 1000,
},
},
],
}
**์กฐํฉ 2: Nuxt.js + Sanity** ๐จ
- Vue ๊ฐ๋ฐ์๋ค์ ์ต์ ์กฐํฉ
- Sanity์ ์ค์๊ฐ ํ์
๊ธฐ๋ฅ
- Nuxt์ ๋ฐ์ด๋ SEO ์ง์
**์กฐํฉ 3: Hugo + Forestry** โก
- ์ด๊ณ ์ ๋น๋ (1000๊ฐ ํ์ด์ง๋ฅผ 1์ด์!)
- Git ๊ธฐ๋ฐ CMS
- ๋งํฌ๋ค์ด ์นํ์
**์กฐํฉ 4: Eleventy + NetlifyCMS** ๐ช
- ์ฌํํ๊ณ ๊ฐ๋ฒผ์ด ์กฐํฉ
- ํ์ต ๊ณก์ ์ด ์๋งํด
- ๋น ๋ฅธ ํ๋กํ ํ์ดํ์ ์ต์
// โ ๋์ ์
const apiKey = 'sk_live_123456789'
fetch(`https://api.example.com/data?key=${apiKey}`)
// โ
์ข์ ์ - ์๋ฒ๋ฆฌ์ค ํจ์ ์ฌ์ฉ
// netlify/functions/getData.js
exports.handler = async function(event, context) {
const apiKey = process.env.API_KEY
const response = await fetch(`https://api.example.com/data?key=${apiKey}`)
return {
statusCode: 200,
body: JSON.stringify(await response.json())
}
}
**2. CORS ์ค์ ** ๐
API ์๋ํฌ์ธํธ์ ์ ์ ํ CORS ์ ์ฑ
์ ์ค์ ํด:
// netlify.toml
[[headers]]
for = "/api/*"
[headers.values]
Access-Control-Allow-Origin = "https://yourdomain.com"
Access-Control-Allow-Methods = "GET, POST"
Access-Control-Allow-Headers = "Content-Type"
**3. ์ฝํ
์ธ ๋ณด์ ์ ์ฑ
(CSP)** ๐ก๏ธ
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
}
]
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
]
},
}
**4. Rate Limiting** โฑ๏ธ
์๋ฒ๋ฆฌ์ค ํจ์์ Rate Limiting์ ๊ตฌํํด:
// Upstash Redis๋ฅผ ์ฌ์ฉํ Rate Limiting
import { Ratelimit } from "@upstash/ratelimit"
import { Redis } from "@upstash/redis"
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "10 s"),
})
export default async function handler(req, res) {
const identifier = req.headers['x-forwarded-for'] || 'anonymous'
const { success } = await ratelimit.limit(identifier)
if (!success) {
return res.status(429).json({ error: 'Too many requests' })
}
// ์ ์ ์ฒ๋ฆฌ
}
### โก ์ฑ๋ฅ ์ต์ ํ ๋ง์คํฐํด๋์ค
**1. ์ด๋ฏธ์ง ์ต์ ํ** ๐ธ
Next.js์ Image ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํด:
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero Image"
width={1200}
height={600}
priority // LCP ๊ฐ์
placeholder="blur" // ๋ธ๋ฌ ํจ๊ณผ
blurDataURL="data:image/jpeg;base64,..." // ๋ธ๋ฌ ๋ฐ์ดํฐ
/>
์ด๋ฏธ์ง ์ต์ ํ ํจ๊ณผ:
- ๐ฏ ์๋ WebP/AVIF ๋ณํ
- ๐ฏ Lazy Loading ๊ธฐ๋ณธ ์ ์ฉ
- ๐ฏ ๋ฐ์ํ ์ด๋ฏธ์ง ์๋ ์์ฑ
- ๐ฏ ํ๊ท **70% ์ฉ๋ ๊ฐ์**!
**2. ์ฝ๋ ์คํ๋ฆฌํ
** ๐ฆ
// ๋์ ์ํฌํธ๋ก ๋ฒ๋ค ํฌ๊ธฐ ์ค์ด๊ธฐ
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false // ํด๋ผ์ด์ธํธ์์๋ง ๋ก๋
})
export default function Page() {
return (
<div>
<h1>My Page</h1>
<HeavyComponent />
</div>
)
}
**3. ํ๋ฆฌํ์นญ ์ ๋ต** ๐ฎ
import Link from 'next/link'
// ์๋ ํ๋ฆฌํ์นญ (๋ทฐํฌํธ์ ๋ค์ด์ค๋ฉด)
<Link href="/blog/post-1" prefetch={true}>
Read More
</Link>
// ๋๋ ํ๋ก๊ทธ๋๋งคํฑํ๊ฒ
import { useRouter } from 'next/router'
const router = useRouter()
router.prefetch('/blog/post-1')
**4. CDN ์บ์ฑ ์ ๋ต** ๐
// vercel.json
{
"headers": [
{
"source": "/static/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/api/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "s-maxage=60, stale-while-revalidate"
}
]
}
]
}
์ด๋ฌํ ์ต์ ํ๋ฅผ ์ ์ฉํ๋ฉด:
- Lighthouse ์ ์: 95+
- First Contentful Paint: 0.8์ด ์ดํ
- Time to Interactive: 1.5์ด ์ดํ
- Total Blocking Time: 50ms ์ดํ
// ํฐํธ ์ต์ ํ ์์
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap', // FOUT ๋ฐฉ์ง
preload: true,
})
export default function App({ Component, pageProps }) {
return (
<main className={inter.className}>
<Component {...pageProps} />
</main>
)
}
// vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": ".next",
"framework": "nextjs",
"regions": ["icn1"] // ์์ธ ๋ฆฌ์
}
**2. Netlify** ๐จ
- ๊ฐ์ฅ ์ค๋๋๊ณ ์์ ์ ์ธ JAMstack ํ๋ซํผ
- Form ์ฒ๋ฆฌ, Identity ๊ธฐ๋ฅ ๋ด์ฅ
- Split Testing ์ง์
- Netlify CMS ํตํฉ
- **๋ฌด๋ฃ ํ๋:** ์ 100GB ๋์ญํญ
// netlify.toml
[build]
command = "npm run build"
publish = "out"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
**3. Cloudflare Pages** ๐
- ๋ฌด์ ํ ๋์ญํญ (๋ฌด๋ฃ!)
- ์ ์ธ๊ณ 275+ ๋ฐ์ดํฐ์ผํฐ
- Workers ํตํฉ์ผ๋ก ์ฃ์ง ์ปดํจํ
- ๋น ๋ฅธ ๋น๋ ์๋
- **๋ฌด๋ฃ ํ๋:** ์ง์ง ๋ฌด์ ํ!
**4. AWS Amplify** โ๏ธ
- AWS ์ํ๊ณ์ ์๋ฒฝํ ํตํฉ
- ๋ฐฑ์๋ ์๋น์ค ์ฝ๊ฒ ์ถ๊ฐ
- ์ปค์คํ
๋๋ฉ์ธ, SSL ์๋
- CI/CD ํ์ดํ๋ผ์ธ ๋ด์ฅ
- **๋ฌด๋ฃ ํ๋:** 12๊ฐ์ ํ๋ฆฌํฐ์ด
### ๐ GitHub Actions๋ก CI/CD ๊ตฌ์ถํ๊ธฐ
์๋ํ๋ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ์ ๋ง๋ค์ด๋ณด์!
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linter
run: npm run lint
- name: Type check
run: npm run type-check
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
CONTENTFUL_SPACE_ID: ${{ secrets.CONTENTFUL_SPACE_ID }}
CONTENTFUL_ACCESS_TOKEN: ${{ secrets.CONTENTFUL_ACCESS_TOKEN }}
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build
path: .next
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build
path: .next
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
์ด ํ์ดํ๋ผ์ธ์:
1. โ
์ฝ๋ ํธ์ ์ ์๋ ์คํ
2. โ
ํ
์คํธ ์คํ
3. โ
๋ฆฐํ
๋ฐ ํ์
์ฒดํฌ
4. โ
๋น๋ ์์ฑ
5. โ
ํ๋ก๋์
๋ฐฐํฌ
// netlify/functions/rebuild.js
exports.handler = async function(event, context) {
const body = JSON.parse(event.body)
// ํน์ ์ฝํ
์ธ ํ์
๋ง ์ฌ๋น๋
if (body.sys.contentType.sys.id === 'blogPost') {
const response = await fetch(process.env.DEPLOY_HOOK_URL, {
method: 'POST'
})
return {
statusCode: 200,
body: JSON.stringify({ message: 'Rebuild triggered' })
}
}
return {
statusCode: 200,
body: JSON.stringify({ message: 'No rebuild needed' })
}
}
### ๐ ๋ค์ค ํ๊ฒฝ ๊ด๋ฆฌ
๊ฐ๋ฐ, ์คํ
์ด์ง, ํ๋ก๋์
ํ๊ฒฝ์ ๋ถ๋ฆฌํด์ ๊ด๋ฆฌํ์:
// package.json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"build:staging": "env-cmd -f .env.staging next build",
"build:production": "env-cmd -f .env.production next build",
"deploy:staging": "vercel --env staging",
"deploy:production": "vercel --prod"
}
}
ํ๊ฒฝ๋ณ ์ค์ ํ์ผ:
# .env.development
NEXT_PUBLIC_API_URL=http://localhost:3000
CONTENTFUL_SPACE_ID=dev_space_id
# .env.staging
NEXT_PUBLIC_API_URL=https://staging.example.com
CONTENTFUL_SPACE_ID=staging_space_id
# .env.production
NEXT_PUBLIC_API_URL=https://example.com
CONTENTFUL_SPACE_ID=prod_space_id
// ํ์ต ์์
1. JSX ๋ฌธ๋ฒ
2. ์ปดํฌ๋ํธ์ Props
3. State์ Hooks
4. ์ด๋ฒคํธ ํธ๋ค๋ง
5. ์กฐ๊ฑด๋ถ ๋ ๋๋ง
6. ๋ฆฌ์คํธ์ Key
// ์ค์ต ํ๋ก์ ํธ
- Todo ์ฑ
- ๋ ์จ ์ฑ
- ์ํ ๊ฒ์ ์ฑ
**Week 5-6: Next.js ์
๋ฌธ** ๐
// ํ์ต ์ฃผ์
1. ํ์ผ ๊ธฐ๋ฐ ๋ผ์ฐํ
2. getStaticProps / getServerSideProps
3. API Routes
4. Image ์ต์ ํ
5. ๋ฐฐํฌ (Vercel)
// ์ค์ต ํ๋ก์ ํธ
- ๊ฐ์ธ ๋ธ๋ก๊ทธ
- ํฌํธํด๋ฆฌ์ค ์ฌ์ดํธ
**Week 7-8: ํค๋๋ฆฌ์ค CMS ์ฒดํ** ๐
1. **Contentful ๋ฌด๋ฃ ๊ณ์ ์์ฑ**
2. **๊ฐ๋จํ ๋ธ๋ก๊ทธ ๋ง๋ค๊ธฐ**
- Content Model ์ค๊ณ
- API ์ฐ๋
- Next.js์ ํตํฉ
// ํ์ต ์ฃผ์
1. Core Web Vitals ์ต์ ํ
2. ์ด๋ฏธ์ง ์ต์ ํ ์ ๋ต
3. ์ฝ๋ ์คํ๋ฆฌํ
4. ์บ์ฑ ์ ๋ต
5. ๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ
// ๋๊ตฌ
- Lighthouse
- WebPageTest
- Bundle Analyzer
**Month 5: ์ค์ ํ๋ก์ ํธ** ๐ผ
๋ณต์กํ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์:
- ์ด์ปค๋จธ์ค ์ฌ์ดํธ
- ์์
๋ฏธ๋์ด ํ๋ซํผ
- ์ฝํ
์ธ ๊ด๋ฆฌ ์์คํ
### ๐ ๊ณ ๊ธ ๋จ๊ณ (3-6๊ฐ์)
**๊ณ ๊ธ ์ํคํ
์ฒ ํจํด** ๐๏ธ
1. **๋ง์ดํฌ๋กํ๋ก ํธ์๋**
- Module Federation
- ๋
๋ฆฝ์ ์ธ ๋ฐฐํฌ
- ํ ๊ฐ ํ์
2. **์๋ฒ๋ฆฌ์ค ์ํคํ
์ฒ**
- AWS Lambda
- Netlify Functions
- Edge Computing
3. **์ค์๊ฐ ๊ธฐ๋ฅ**
- WebSocket
- Server-Sent Events
- Supabase Realtime
**DevOps์ ์ธํ๋ผ** ๐ ๏ธ
// ํ์ต ์์ญ
1. Docker ๊ธฐ์ด
2. CI/CD ํ์ดํ๋ผ์ธ
3. ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
4. ์๋ฌ ํธ๋ํน (Sentry)
5. ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง (Vercel Analytics)
### ๐ ์ถ์ฒ ํ์ต ๋ฆฌ์์ค
**๊ณต์ ๋ฌธ์** ๐
- Next.js ๊ณต์ ๋ฌธ์: https://nextjs.org/docs
- Contentful ๋ฌธ์: https://www.contentful.com/developers/docs/
- JAMstack.org: https://jamstack.org/
**์จ๋ผ์ธ ๊ฐ์** ๐ฅ
- Frontend Masters: "Complete Intro to React"
- Udemy: "Next.js & React - The Complete Guide"
- egghead.io: "Build a Modern User Interface with Chakra UI"
**์ ํ๋ธ ์ฑ๋** ๐บ
- Fireship (๋น ๋ฅธ ๊ฐ๋
์ค๋ช
)
- Web Dev Simplified (์ด๋ณด์ ์นํ์ )
- Traversy Media (์ค์ ํ๋ก์ ํธ)
- Lee Robinson (Next.js ์ ๋ฌธ)
**์ปค๋ฎค๋ํฐ** ๐ฅ
- JAMstack Discord
- Next.js Discord
- Reddit: r/nextjs, r/reactjs
- Dev.to (๋ธ๋ก๊ทธ ํ๋ซํผ)
**์ค์ต ํ๋ซํผ** ๐ป
- CodeSandbox (์จ๋ผ์ธ IDE)
- StackBlitz (Next.js ํ
ํ๋ฆฟ)
- GitHub (์คํ์์ค ํ๋ก์ ํธ)
// Vercel Edge Functions ์์
export const config = {
runtime: 'edge',
}
export default async function handler(req) {
const country = req.geo.country
// ์ฌ์ฉ์ ์์น์ ๋ฐ๋ผ ๋ค๋ฅธ ์ฝํ
์ธ ์ ๊ณต
const content = await getLocalizedContent(country)
return new Response(JSON.stringify(content), {
headers: { 'content-type': 'application/json' },
})
}
**์ฅ์ :**
- ๐ ์ด์ ์ง์ฐ (10-50ms)
- ๐ ๊ธ๋ก๋ฒ ๋ถ์ฐ
- ๐ฐ ๋น์ฉ ํจ์จ์
- ๐ ๋ณด์ ๊ฐํ
**2. AI ๊ธฐ๋ฐ ์ฝํ
์ธ ์์ฑ** ๐ค
ํค๋๋ฆฌ์ค CMS์ AI๊ฐ ํตํฉ๋๊ณ ์์ด:
- ์๋ ๋ฉํ๋ฐ์ดํฐ ์์ฑ
- ์ด๋ฏธ์ง alt ํ
์คํธ ์๋ ์์ฑ
- ์ฝํ
์ธ ์ถ์ฒ
- SEO ์ต์ ํ ์ ์
**3. Visual Editing** ๐จ
์ฝ๋ ์์ด ๋น์ฃผ์ผํ๊ฒ ํธ์งํ๋ ๋๊ตฌ๋ค:
- **Builder.io**: ๋๋๊ทธ ์ค ๋๋กญ ์๋ํฐ
- **Plasmic**: ๋์์ธ โ ์ฝ๋ ์๋ ๋ณํ
- **Storyblok**: ๋น์ฃผ์ผ ์๋ํฐ ๋ด์ฅ
// ๋์ ์ - ๋ชจ๋ ๊ฑธ ํด๋ผ์ด์ธํธ์์ ์ฒ๋ฆฌ
function BlogPage() {
const [posts, setPosts] = useState([])
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => setPosts(data))
}, [])
return <div>{posts.map(post => ...)}</div>
}
**๋ฌธ์ ์ :**
- SEO์ ๋ถ๋ฆฌํด
- ์ด๊ธฐ ๋ก๋ฉ์ด ๋๋ ค
- JavaScript ์์ผ๋ฉด ์๋ฌด๊ฒ๋ ์ ๋ณด์ฌ
**ํด๊ฒฐ์ฑ
:**
// ์ข์ ์ - ์๋ฒ์์ ๋ฏธ๋ฆฌ ๋ ๋๋ง
export async function getStaticProps() {
const posts = await fetchPosts()
return {
props: { posts },
revalidate: 60
}
}
function BlogPage({ posts }) {
return <div>{posts.map(post => ...)}</div>
}
### โ ์ค์ 2: ์ด๋ฏธ์ง ์ต์ ํ ๋ฌด์
**๋ฌธ์ :**
// ๋์ ์
<img src="/huge-image.jpg" alt="Hero" />
**๋ฌธ์ ์ :**
- 5MB ์ด๋ฏธ์ง๋ฅผ ๊ทธ๋๋ก ๋ก๋
- ๋ชจ๋ฐ์ผ์์๋ ๊ฐ์ ํฌ๊ธฐ
- LCP ์ ์ ํญ๋ฝ
**ํด๊ฒฐ์ฑ
:**
// ์ข์ ์
import Image from 'next/image'
<Image
src="/huge-image.jpg"
alt="Hero"
width={1200}
height={600}
priority
quality={85}
placeholder="blur"
/>
**๊ฒฐ๊ณผ:**
- ์๋ WebP ๋ณํ
- ๋ฐ์ํ ์ด๋ฏธ์ง
- Lazy Loading
- 70-80% ์ฉ๋ ๊ฐ์!
### โ ์ค์ 3: API ํค ๋
ธ์ถ
**๋ฌธ์ :**
// ์ํ! ํด๋ผ์ด์ธํธ ์ฝ๋์ API ํค
const API_KEY = 'sk_live_abc123...'
fetch(`https://api.example.com/data?key=${API_KEY}`)
**ํด๊ฒฐ์ฑ
:**
// ์๋ฒ๋ฆฌ์ค ํจ์ ์ฌ์ฉ
// pages/api/getData.js
export default async function handler(req, res) {
const API_KEY = process.env.SECRET_API_KEY
const response = await fetch(
`https://api.example.com/data?key=${API_KEY}`
)
const data = await response.json()
res.json(data)
}
// ํด๋ผ์ด์ธํธ์์๋
fetch('/api/getData')
### โ ์ค์ 4: ๋น๋ ์๊ฐ ํญ๋ฐ
**๋ฌธ์ :**
- 10,000๊ฐ ํ์ด์ง๋ฅผ ๋ชจ๋ ๋น๋ ํ์์ ์์ฑ
- ๋น๋ ์๊ฐ: 2์๊ฐ+
- ๋ฐฐํฌ ๋ถ๊ฐ๋ฅ
**ํด๊ฒฐ์ฑ
:**
// getStaticPaths์์ fallback ์ฌ์ฉ
export async function getStaticPaths() {
// ์ธ๊ธฐ ์๋ ํ์ด์ง๋ง ๋ฏธ๋ฆฌ ์์ฑ
const popularPosts = await getPopularPosts(100)
return {
paths: popularPosts.map(post => ({
params: { slug: post.slug }
})),
fallback: 'blocking' // ๋๋จธ์ง๋ ์์ฒญ ์ ์์ฑ
}
}
### โ ์ค์ 5: ์บ์ฑ ์ ๋ต ๋ถ์ฌ
**๋ฌธ์ :**
- API ํธ์ถ์ ๋งค๋ฒ ์๋ก ํจ
- ๋ถํ์ํ ์ฌ๋น๋
- ๋น์ฉ ์ฆ๊ฐ
**ํด๊ฒฐ์ฑ
:**
// SWR ์ฌ์ฉ (ํด๋ผ์ด์ธํธ ์บ์ฑ)
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 60000 // 1๋ถ๋ง๋ค ๊ฐฑ์
})
if (error) return <div>Failed to load</div>
if (!data) return <div>Loading...</div>
return <div>Hello {data.name}!</div>
}
// ์๋ฒ ์ฌ์ด๋ ์บ์ฑ
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 3600 // 1์๊ฐ๋ง๋ค ์ฌ๊ฒ์ฆ
}
}
// next-seo ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ
import { NextSeo, ArticleJsonLd } from 'next-seo'
function BlogPost({ post }) {
return (
<>
<NextSeo
title={post.title}
description={post.excerpt}
canonical={`https://example.com/blog/${post.slug}`}
openGraph={{
type: 'article',
article: {
publishedTime: post.publishedAt,
authors: [post.author.name],
},
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
],
}}
/>
<ArticleJsonLd
url={`https://example.com/blog/${post.slug}`}
title={post.title}
images={[post.coverImage]}
datePublished={post.publishedAt}
authorName={post.author.name}
description={post.excerpt}
/>
{/* ์ฝํ
์ธ */}
</>
)
}
### โ ์ค์ 7: ์ ๊ทผ์ฑ ๋ฌด์
**๋ฌธ์ :**
- ํค๋ณด๋ ๋ค๋น๊ฒ์ด์
๋ถ๊ฐ
- ์คํฌ๋ฆฐ ๋ฆฌ๋ ์ง์ ์์
- ์์ ๋๋น ๋ถ์กฑ
**ํด๊ฒฐ์ฑ
:**
// ์ ๊ทผ์ฑ์ ๊ณ ๋ คํ ์ปดํฌ๋ํธ
<button
onClick={handleClick}
aria-label="Close dialog"
aria-pressed={isPressed}
>
<span aria-hidden="true">ร</span>
</button>
<img
src="/image.jpg"
alt="Descriptive text for screen readers"
loading="lazy"
/>
// ํฌ์ปค์ค ๊ด๋ฆฌ
import { useRef, useEffect } from 'react'
function Modal({ isOpen }) {
const closeButtonRef = useRef()
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus()
}
}, [isOpen])
return (
<div role="dialog" aria-modal="true">
<button ref={closeButtonRef}>Close</button>
</div>
)
}
๐
Happy Coding!
ํค๋๋ฆฌ์ค CMS์ JAMstack์ผ๋ก ๋ฉ์ง ์น์ ๋ง๋ค์ด๋ณด์!
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.