정적 블로그에서 백엔드 통신하기 - 일반 웹과의 결정적 차이
TL;DR
- 문제: 정적 블로그에서 API 키를 프론트엔드에 넣으면 개발자 도구에 그대로 노출됨
- 원인: 로그인이 없으니 사용자 인증(JWT)이 불가능, 백엔드 서버도 없음
- 해결: Netlify Functions로 API 키를 환경변수에 숨기고 서버리스 백엔드 구축
- 효과: API 키 보호 + 서버 운영 부담 제로 + 월 100원 미만 비용
- 한계: 콜드 스타트(최대 2초), Netlify 종속성, 복잡한 로직엔 부적합
글 머리말
“왜 Netlify Functions를 써야 하나요? 그냥 axios로 API 호출하면 안 되나요?”
혼자 블로그를 개발하다가 문득 이런 의문이 들었습니다. 저는 7년 동안 일반 웹 애플리케이션을 개발해왔거든요. 로그인하면 JWT 토큰 받고, 그 토큰으로 백엔드 API 호출하고. 너무 당연하게 해왔던 방식이죠.
그런데 정적 블로그로 넘어오니까 당연했던 것들이 작동하지 않더군요. 로그인? 없습니다. JWT 토큰? 발급할 수 없습니다. 백엔드 서버? 그것도 없습니다.
처음에는 “그냥 프론트엔드에서 API 키 넣어서 호출하면 되지 않나?” 싶었습니다. 실제로 그렇게 했다가 크롬 개발자 도구에서 API 키가 그대로 노출되는 걸 보고 식겁했습니다.
이번 글에서는 일반 웹 애플리케이션과 정적 블로그의 백엔드 통신 방식 차이를 정리해보았습니다. 저처럼 “왜 굳이 Netlify Functions를 써야 하지?”라는 의문이 드셨다면, 이 글이 답이 될 겁니다.
일반 웹 애플리케이션의 통신 구조
로그인 기반 인증
일반 웹 애플리케이션(React SPA, Vue.js 등)에서는 이런 흐름으로 작동합니다.
sequenceDiagram
participant U as 사용자
participant F as 프론트엔드
participant B as 백엔드 서버
participant DB as 데이터베이스
U->>F: 1. 로그인 (ID/PW)
F->>B: 2. POST /api/login
B->>DB: 3. 사용자 인증
DB-->>B: 4. 인증 성공
B-->>F: 5. JWT 토큰 발급
F->>F: 6. 토큰 저장
U->>F: 7. 데이터 요청
F->>B: 8. GET /api/data + JWT
B->>B: 9. JWT 검증
B->>DB: 10. 데이터 조회
DB-->>B: 11. 결과 반환
B-->>F: 12. 데이터 전달
F-->>U: 13. 화면 표시코드 예시: 일반 웹 애플리케이션
// 1. 로그인
async function login(username, password) {
const response = await axios.post('/api/login', { username, password })
const { token } = response.data
// 2. JWT 토큰 저장
localStorage.setItem('authToken', token)
}
// 3. API 호출 시 토큰 사용
async function fetchUserData() {
const token = localStorage.getItem('authToken') // 사용자별 인증키
const response = await axios.get('/api/user/profile', {
headers: {
Authorization: `Bearer ${token}`, // 개인별 인증 토큰
},
})
return response.data
}이 방식의 핵심은 사용자별 인증입니다.
| 구성 요소 | 역할 |
|---|---|
| JWT 토큰 | 사용자를 식별하는 고유한 인증키 |
| localStorage | 브라우저에 토큰 저장 |
| Authorization Header | 모든 API 요청에 토큰 포함 |
| 백엔드 검증 | 토큰이 유효한지 확인 후 데이터 제공 |
너무 익숙한 흐름이죠. 저도 수년간 이 방식으로 개발해왔습니다.
API 키는 어디에 저장할까?
일반 웹 애플리케이션에서 Supabase 같은 외부 API를 호출할 때는 이렇게 합니다.
// 절대 이렇게 하면 안 됨!
const SUPABASE_KEY = 'eyJhbGciOi...' // 소스코드에 노출!
async function saveData(email) {
await fetch('https://api.supabase.co/rest/v1/subscribers', {
headers: {
apikey: SUPABASE_KEY, // 개발자 도구에서 보임!
Authorization: `Bearer ${SUPABASE_KEY}`,
},
body: JSON.stringify({ email }),
})
}문제점:
- 소스코드가 브라우저에 전부 노출됨
- 개발자 도구에서 API 키를 복사할 수 있음
- 불특정 다수가 API 키를 악용 가능
해결책: 백엔드 서버에서만 API 키 사용
// 프론트엔드 (API 키 없음)
async function saveData(email) {
await fetch('/api/newsletter', {
// 내 서버로 요청
method: 'POST',
body: JSON.stringify({ email }),
})
}
// 백엔드 (Node.js 서버)
app.post('/api/newsletter', async (req, res) => {
const { email } = req.body
const SUPABASE_KEY = process.env.SUPABASE_KEY // 환경변수 (서버에만 존재)
await fetch('https://api.supabase.co/rest/v1/subscribers', {
headers: {
apikey: SUPABASE_KEY,
Authorization: `Bearer ${SUPABASE_KEY}`,
},
body: JSON.stringify({ email }),
})
res.json({ success: true })
})| 장소 | API 키 위치 | 노출 여부 |
|---|---|---|
| 프론트엔드 (브라우저) | API 키 없음 | 소스코드 공개 |
| 백엔드 (서버) | 환경변수 | 외부 접근 불가 |
핵심 요약
- 사용자별 인증: JWT 토큰으로 “누가 요청하는지” 식별
- API 키 보호: 백엔드 서버의 환경변수에 저장
- 프론트엔드는 깨끗: 민감한 정보 없이 내 서버만 호출
정적 블로그의 딜레마
백엔드 서버가 없다
정적 블로그(Gatsby, Next.js SSG 모드)는 빌드 시점에 HTML 파일로 변환됩니다.
# 빌드 과정
gatsby build
→ src/ (React 코드) → public/ (정적 HTML/CSS/JS)
# 결과물
public/
├── index.html
├── about/index.html
├── blog/post-1/index.html
└── static/
└── app.js ← 이 파일이 브라우저에서 실행됨문제:
- Node.js 서버가 없음 (서버 API 엔드포인트 없음)
- 모든 JavaScript 코드가 브라우저에서 실행됨
- API 키를 코드에 넣으면 누구나 볼 수 있음
처음에 저도 “환경변수 쓰면 되지 않나?” 싶었습니다. 하지만 Gatsby에서 환경변수를 쓰면 빌드 시점에 값이 코드에 박히게 됩니다. 결국 브라우저에서 전부 노출되는 거죠.
그럼 이렇게 하면 어떻게 될까요?
// 정적 블로그에서 절대 금지!
const GA4_KEY = 'YOUR_GOOGLE_API_KEY'
function BlogStats() {
const [stats, setStats] = useState({})
useEffect(() => {
fetch(
'https://analyticsdata.googleapis.com/v1beta/properties/123456789:runReport',
{
headers: {
Authorization: `Bearer ${GA4_KEY}`, // 크롬 개발자 도구에 노출!
},
}
)
.then(res => res.json())
.then(setStats)
}, [])
return <div>{stats.todayVisitors}</div>
}결과:
- 빌드된
app.js파일에 API 키가 그대로 포함됨 - 누구나 개발자 도구에서 키를 복사할 수 있음
- 불특정 다수의 악의적 사용 가능 (API 할당량 소진, 데이터 삭제 등)
저도 처음에 이렇게 했다가 GA4 API 키가 노출되는 걸 보고 깜짝 놀랐습니다. 다행히 배포 전에 발견해서 큰 사고는 없었지만요.
로그인도 없다
일반 웹과 달리 블로그는 누구나 접속 가능합니다.
| 일반 웹 | 정적 블로그 |
|---|---|
| 로그인 필요 | 로그인 없음 |
| JWT 토큰으로 사용자 식별 | 사용자 구분 불가 |
| 토큰 없으면 API 호출 차단 | 차단 메커니즘 없음 |
| 사용자별 권한 관리 | 권한 개념 없음 |
정적 블로그의 특성:
- URL만 알면 누구나 접속 가능
- 사용자 인증 개념이 없음
- JWT 토큰 발급 불가능
- 백엔드 서버가 없어서 환경변수 사용 불가
그럼 어떻게 해야 할까요? 여기서 Netlify Functions가 등장합니다.
핵심 요약
- 서버가 없음: 빌드 결과물은 정적 파일뿐
- API 키 노출 위험: 코드에 넣으면 전부 공개
- 로그인 없음: 사용자별 인증 불가능
Netlify Functions - 서버리스 백엔드
서버 없이 서버 기능 사용하기
Netlify Functions는 서버를 직접 운영하지 않고도 서버 기능을 사용할 수 있게 해줍니다.
graph TB
subgraph 정적 블로그 구조
A[정적 HTML/CSS/JS]
B[Netlify Functions]
C[환경변수 저장소]
end
subgraph 외부 서비스
D[Supabase]
E[SendGrid]
F[Google Analytics]
end
A -->|API 키 없음| B
B -->|환경변수 사용| C
C -.->|API 키 저장| B
B -->|인증된 요청| D
B -->|인증된 요청| E
B -->|인증된 요청| F
style A fill:#e3f2fd
style B fill:#fff3e0
style C fill:#f1f8e9핵심 개념:
- 정적 파일은 API 키 없이 Netlify Functions만 호출
- Netlify Functions는 환경변수에서 API 키를 가져옴
- 외부 서비스 호출은 Netlify Functions에서만 수행
즉, Netlify Functions가 백엔드 서버 역할을 하는 겁니다. 다만 서버를 직접 운영하지 않아도 되죠.
대안 검토: 왜 Netlify Functions인가?
처음엔 여러 선택지를 고민했습니다.
| 항목 | Netlify Functions | Vercel Functions | AWS Lambda | Node.js 서버 | 선택 이유 |
|---|---|---|---|---|---|
| 비용 | 무료 (월 12만 호출) | 무료 (월 10만 호출) | 무료 (월 100만 호출) | 월 $10+ (EC2 t3.micro) | ✅ 선택 |
| 배포 | 자동 (Git push) | 자동 (Git push) | 수동 (CI/CD 구성) | 수동 (SSH 배포) | ✅ 편함 |
| 콜드 스타트 | 1~2초 | 1~2초 | 0.5~1초 | 없음 | ⚠️ 감수 |
| 학습 곡선 | 낮음 | 낮음 | 높음 (IAM, VPC 등) | 중간 | ✅ 쉬움 |
| 통합성 | Gatsby와 완벽 통합 | Next.js 최적화 | 독립적 | 독립적 | ✅ 선택 |
| 운영 부담 | 없음 | 없음 | 낮음 | 높음 (모니터링, 배포) | ✅ 선택 |
왜 AWS Lambda를 안 했나?
- 가장 저렴하고 빠르지만, IAM 설정, VPC 구성, CloudWatch 로그 등 학습 곡선이 가파름
- 개인 블로그에는 오버엔지니어링
왜 Node.js 서버를 안 했나?
- 익숙하긴 하지만 서버 운영 부담 (모니터링, 배포, 스케일링, 보안 패치)
- 월 $10+ 비용 vs 무료 (Netlify) = 손해
왜 Vercel Functions를 안 했나?
- Vercel은 Next.js에 최적화됨
- 저는 이미 Gatsby + Netlify 조합을 사용 중
Netlify Functions를 선택한 이유:
- ✅ Gatsby와 완벽 통합 (빌드 시 자동 배포)
- ✅ Git push만 하면 배포 완료
- ✅ 환경변수 관리 UI 제공
- ✅ 무료 플랜 충분 (월 12만 호출)
- ✅ 운영 부담 제로
실제 코드 비교
일반 웹 (Node.js 서버)
프로젝트 구조:
frontend/ ← React 코드 (브라우저)
└── src/
└── components/
backend/ ← Node.js 서버
├── server.js
├── routes/
└── .env ← API 키 저장정적 블로그 (Netlify Functions)
프로젝트 구조:
src/ ← React 코드 (빌드되면 HTML/JS)
└── components/
netlify/ ← 서버리스 함수 (Netlify에서 실행)
└── functions/
└── newsletter-submit.js
.env ← 로컬 개발용 (Git에 업로드 안 함)
Netlify 대시보드 ← 배포 환경 변수 (서버에만 존재)코드 비교:
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 프론트엔드 (공통 - API 키 없음)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 일반 웹
async function submitNewsletter(email) {
await fetch('https://myserver.com/api/newsletter', {
// 내 Node.js 서버
method: 'POST',
body: JSON.stringify({ email }),
})
}
// 정적 블로그
async function submitNewsletter(email) {
await fetch('/.netlify/functions/newsletter-submit', {
// Netlify Function
method: 'POST',
body: JSON.stringify({ email }),
})
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 백엔드 (API 키 사용)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 일반 웹 - Node.js 서버 (backend/server.js)
app.post('/api/newsletter', async (req, res) => {
const { email } = req.body
const SUPABASE_KEY = process.env.SUPABASE_KEY // 환경변수
await fetch('https://api.supabase.co/rest/v1/subscribers', {
headers: {
apikey: SUPABASE_KEY,
Authorization: `Bearer ${SUPABASE_KEY}`,
},
body: JSON.stringify({ email }),
})
res.json({ success: true })
})
// 정적 블로그 - Netlify Function (netlify/functions/newsletter-submit.js)
exports.handler = async event => {
const { email } = JSON.parse(event.body)
const SUPABASE_KEY = process.env.SUPABASE_KEY // 환경변수 (동일!)
await fetch('https://api.supabase.co/rest/v1/subscribers', {
headers: {
apikey: SUPABASE_KEY,
Authorization: `Bearer ${SUPABASE_KEY}`,
},
body: JSON.stringify({ email }),
})
return {
statusCode: 200,
body: JSON.stringify({ success: true }),
}
}차이점:
| 구분 | 일반 웹 | 정적 블로그 |
|---|---|---|
| 백엔드 형태 | Node.js 서버 (EC2, ECS 등) | Netlify Functions (서버리스) |
| 서버 관리 | 직접 운영 (EC2, 모니터링, 배포) | Netlify가 자동 관리 |
| 환경변수 위치 | 서버 .env 또는 환경 설정 | Netlify 대시보드 |
| 비용 | 서버 운영 비용 (월 5만원+) | 무료 티어 충분 |
| 확장성 | 수동 스케일링 | 자동 스케일링 |
보시다시피 백엔드 로직은 거의 동일합니다. 차이는 서버를 직접 운영하느냐, Netlify가 관리해주느냐 뿐이죠.
환경변수 설정 방법
로컬 개발 (.env 파일)
# .env (Git에 업로드하지 않음!)
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SENDGRID_API_KEY=SG.aBcDeFgHiJkLmNoPqRsTuVwXyZ...
GA4_CREDENTIALS_JSON=eyJjbGllbnRfZW1haWwiOi...Netlify 배포 환경
https://app.netlify.com/sites/your-site/settings/deploys
→ Environment variables 섹션
→ Add variable 클릭
Key: SUPABASE_SERVICE_KEY
Value: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Key: SENDGRID_API_KEY
Value: SG.aBcDeFgHiJkLmNoPqRsTuVwXyZ...중요:
.env파일은.gitignore에 추가하여 Git에 업로드하지 않음- Netlify 대시보드에서 환경변수를 수동으로 설정
- 환경변수는 Netlify Functions에서만 접근 가능 (브라우저에서 접근 불가)
핵심 요약
- Netlify Functions = 서버리스 백엔드: 서버 없이 서버 기능 사용
- 환경변수로 API 키 보호: 브라우저에 노출되지 않음
- 백엔드 로직은 동일: 다만 서버 운영 부담이 없음
실전 예시: 블로그 통계 기능
요구사항
Google Analytics 4 API를 사용해서 블로그 방문자 통계를 표시합니다.
- 오늘 방문자 수
- 총 방문자 수
- 인기 포스팅 Top 5
실제로 저희 블로그에 구현한 기능입니다.
일반 웹 방식 (Node.js)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 프론트엔드 (src/components/BlogStats.jsx)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function BlogStats() {
const [stats, setStats] = useState({})
useEffect(() => {
fetch('https://myserver.com/api/blog-stats') // Node.js 서버
.then(res => res.json())
.then(setStats)
}, [])
return (
<div>
<p>오늘 방문자: {stats.todayVisitors}</p>
<p>총 방문자: {stats.totalVisitors}</p>
</div>
)
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 백엔드 (backend/routes/blog-stats.js)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const { BetaAnalyticsDataClient } = require('@google-analytics/data')
app.get('/api/blog-stats', async (req, res) => {
// 환경변수에서 GA4 인증 정보 가져오기
const credentials = JSON.parse(process.env.GA4_CREDENTIALS_JSON)
const analyticsDataClient = new BetaAnalyticsDataClient({
credentials: {
client_email: credentials.client_email,
private_key: credentials.private_key,
},
})
// GA4에서 데이터 가져오기
const [todayData, totalData] = await Promise.all([
analyticsDataClient.runReport({
property: `properties/${process.env.GA4_PROPERTY_ID}`,
dateRanges: [{ startDate: 'today', endDate: 'today' }],
metrics: [{ name: 'activeUsers' }],
}),
analyticsDataClient.runReport({
property: `properties/${process.env.GA4_PROPERTY_ID}`,
dateRanges: [{ startDate: '2020-01-01', endDate: 'today' }],
metrics: [{ name: 'totalUsers' }],
}),
])
res.json({
todayVisitors: parseInt(
todayData[0].rows?.[0]?.metricValues?.[0]?.value || '0'
),
totalVisitors: parseInt(
totalData[0].rows?.[0]?.metricValues?.[0]?.value || '0'
),
})
})정적 블로그 방식 (Netlify Functions)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 프론트엔드 (src/components/BlogStats.jsx)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function BlogStats() {
const [stats, setStats] = useState({})
useEffect(() => {
fetch('/.netlify/functions/blog-stats') // Netlify Function
.then(res => res.json())
.then(setStats)
}, [])
return (
<div>
<p>오늘 방문자: {stats.todayVisitors}</p>
<p>총 방문자: {stats.totalVisitors}</p>
</div>
)
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Netlify Function (netlify/functions/blog-stats.js)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const { BetaAnalyticsDataClient } = require('@google-analytics/data')
exports.handler = async event => {
// 환경변수에서 GA4 인증 정보 가져오기 (일반 웹과 동일!)
const credentials = JSON.parse(process.env.GA4_CREDENTIALS_JSON)
const analyticsDataClient = new BetaAnalyticsDataClient({
credentials: {
client_email: credentials.client_email,
private_key: credentials.private_key,
},
})
// GA4에서 데이터 가져오기 (일반 웹과 동일!)
const [todayData, totalData] = await Promise.all([
analyticsDataClient.runReport({
property: `properties/${process.env.GA4_PROPERTY_ID}`,
dateRanges: [{ startDate: 'today', endDate: 'today' }],
metrics: [{ name: 'activeUsers' }],
}),
analyticsDataClient.runReport({
property: `properties/${process.env.GA4_PROPERTY_ID}`,
dateRanges: [{ startDate: '2020-01-01', endDate: 'today' }],
metrics: [{ name: 'totalUsers' }],
}),
])
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
todayVisitors: parseInt(
todayData[0].rows?.[0]?.metricValues?.[0]?.value || '0'
),
totalVisitors: parseInt(
totalData[0].rows?.[0]?.metricValues?.[0]?.value || '0'
),
}),
}
}차이점 요약:
| 구분 | 일반 웹 | 정적 블로그 |
|---|---|---|
| 프론트엔드 API 호출 | https://myserver.com/api/blog-stats | /.netlify/functions/blog-stats |
| 백엔드 코드 | app.get('/api/blog-stats', ...) | exports.handler = async (event) => ... |
| 응답 방식 | res.json({ ... }) | return { statusCode: 200, body: JSON.stringify({ ... }) } |
| 서버 운영 | EC2, ECS 등 직접 관리 | Netlify가 자동 관리 |
핵심:
- 백엔드 로직은 거의 동일함 (환경변수 사용, API 호출)
- 차이는 서버 운영 방식 (직접 관리 vs 서버리스)
내 프로젝트에 바로 적용하기
체크리스트
정적 블로그에서 백엔드 통신이 필요할 때:
- API 키가 필요한 외부 서비스인가?
- Netlify Functions 폴더 구조가 있는가? (
netlify/functions/) -
.env파일이.gitignore에 추가되어 있는가? - Netlify 대시보드에 환경변수가 설정되어 있는가?
- 프론트엔드에서
/.netlify/functions/경로로 호출하는가?
구현 순서
1단계: 폴더 구조 생성
mkdir -p netlify/functions2단계: 함수 파일 생성
exports.handler = async event => {
const API_KEY = process.env.MY_API_KEY
// 외부 API 호출
const response = await fetch('https://external-api.com/data', {
headers: { Authorization: `Bearer ${API_KEY}` },
})
const data = await response.json()
return {
statusCode: 200,
body: JSON.stringify(data),
}
}3단계: 프론트엔드에서 호출
const response = await fetch('/.netlify/functions/my-api')
const data = await response.json()주의사항
프론트엔드에 API 키 넣기
// 절대 금지!
const API_KEY = 'sk-1234567890' // 빌드 시 코드에 박힘
fetch(url, { headers: { Authorization: API_KEY } })환경변수를 프론트엔드에서 사용
// 절대 금지! (Gatsby에서는 빌드 시 값이 주입됨)
const key = process.env.GATSBY_API_KEY // 브라우저에서 보임.env 파일 Git에 업로드
# .gitignore에 반드시 추가!
.env
.env.local
.env.production트러블슈팅
Q. “Netlify Functions가 실행 안 돼요”
netlify.toml에 functions 디렉토리 설정 확인- 함수 파일이
exports.handler형태인지 확인
Q. “환경변수가 undefined로 나와요”
- 로컬:
.env파일에 변수가 있는지 확인 - 배포: Netlify 대시보드에 환경변수 설정했는지 확인
- 재배포가 필요할 수 있음
Q. “CORS 에러가 나요”
- Netlify Functions는 같은 도메인이라 CORS 문제 없어야 함
- 외부 API 호출 시 해당 서비스의 CORS 정책 확인
이 접근의 아쉬운 점
1. 콜드 스타트 지연 (최대 2초)
Netlify Functions의 가장 큰 단점입니다.
콜드 스타트란?
한동안 호출이 없으면 함수가 “잠들고”, 다음 호출 시 “깨어나는” 시간이 필요합니다.
- 첫 호출: 1~2초 지연
- 이후 호출 (5분 이내): 즉시 응답
문제 상황:
- 뉴스레터 구독 버튼 클릭 → 2초간 응답 없음
- 사용자: “어? 안 먹혔나?” (재클릭 → 중복 요청)
해결책:
- 로딩 스피너 표시 (“처리 중입니다…“)
- 버튼 비활성화 (중복 클릭 방지)
- 또는 Netlify Pro 플랜 ($19/월) → 콜드 스타트 0초
우리 선택:
무료 플랜 + 로딩 UI로 충분합니다. 하루 방문자 500~1000명 수준에서는 문제없습니다.
2. Netlify 종속성
Netlify가 문제 생기면?
- 서비스 장애
- 가격 정책 변경 (무료 플랜 축소)
- 서비스 종료 (가능성 낮지만)
대응책:
- Functions 코드를 표준 Node.js로 작성 (다른 플랫폼 이전 쉽게)
- Vercel/AWS Lambda로 마이그레이션 준비 (1~2시간 작업)
실제 경험:
2년 운영 중 장애 없음. Netlify는 안정적입니다.
3. 복잡한 로직엔 부적합
Netlify Functions는 간단한 작업에 최적화되어 있습니다.
| 용도 | 적합성 | 이유 |
|---|---|---|
| API 키 숨기기 | ✅ 완벽 | 단순 프록시 |
| 뉴스레터 구독 | ✅ 완벽 | 단순 POST 요청 |
| 블로그 통계 조회 | ✅ 완벽 | 단순 GET 요청 |
| 이미지 리사이징 | ⚠️ 제한적 | 메모리 제한 (1GB) |
| 대용량 파일 처리 | ❌ 부적합 | 타임아웃 (10초) |
| 웹소켓 | ❌ 불가능 | 단방향 요청만 지원 |
만약 복잡한 로직이 필요하다면?
AWS Lambda (타임아웃 15분, 메모리 10GB) 또는 Node.js 서버를 고려해야 합니다.
4. 디버깅이 어렵다
로컬 개발은 쉽지만, 배포 후 문제 발생 시:
- Netlify 로그만 확인 가능 (제한적)
- 실시간 디버깅 불가능
- 에러 추적이 서버만큼 세밀하지 않음
해결책:
- 충분한 로깅 추가 (
console.log) - Sentry 같은 에러 모니터링 도구 연동
- 로컬에서
netlify dev로 충분히 테스트
5. 결국 “충분히 좋은 해결”을 선택했다
Netlify Functions는 완벽하지 않습니다. 하지만:
- ✅ 개인 블로그 규모에는 완벽
- ✅ 서버 운영 부담 제로
- ✅ 비용 거의 무료
- ✅ 배포가 너무 쉬움 (Git push)
만약 하루 방문자 1만 명 이상이라면?
그때는 AWS Lambda나 Node.js 서버를 고민해야 할 겁니다.
지금은 이 정도로 충분합니다.
시스템 점검 체크리스트
저도 배포 전에 이 항목들을 꼭 확인합니다. Netlify Functions를 사용한다면 참고하시면 좋을 것 같습니다.
- 환경변수 분리: API 키가 .env 파일과 Netlify 대시보드에만 있고, 코드에는 없는가?
- .gitignore 설정: .env 파일이 Git에 업로드되지 않도록 설정했는가?
- 에러 핸들링: Functions에서 try-catch로 에러를 잡고 적절한 statusCode를 반환하는가?
- CORS 설정: 필요한 경우 headers에 Access-Control-Allow-Origin을 설정했는가?
- 콜드 스타트 대응: 로딩 UI와 버튼 비활성화로 중복 클릭을 방지했는가?
결론
일반 웹과 정적 블로그의 가장 큰 차이는 인증의 주체입니다.
graph LR
subgraph 일반 웹
A1[사용자 인증] --> B1[JWT 토큰]
B1 --> C1[백엔드 API 호출]
end
subgraph 정적 블로그
A2[서버 인증] --> B2[환경변수]
B2 --> C2[서버리스 함수 호출]
end
style A1 fill:#e3f2fd
style A2 fill:#fff3e0- 일반 웹: “당신이 누구인지”를 증명해야 함 (JWT 토큰)
- 정적 블로그: “서버가 신뢰할 수 있는지”만 중요 (환경변수)
왜 Netlify Functions가 필요한가?
솔직히 처음에는 “그냥 Node.js 서버 하나 띄울까?” 고민했습니다. 익숙하니까요.
하지만 Netlify Functions를 선택한 이유는 명확했습니다:
- 운영 부담 제로: 서버 모니터링, 배포 파이프라인, 스케일링… 다 Netlify가 해줌
- 비용 효율: 월 100원 미만 (EC2 t3.micro도 월 2만원+)
- 자동 스케일링: 갑자기 트래픽이 늘어도 걱정 없음
- 빠른 개발: 함수 하나 만들면 바로 배포
“최고의 서버는 운영하지 않는 서버”라는 말이 실감났습니다.
로그인이 없는 이유
일반 웹은 특정 사용자에게만 서비스를 제공합니다. 내 데이터를 보려면 내가 누군지 증명해야 하죠.
하지만 블로그는 누구나 접속 가능한 공개 콘텐츠입니다. 굳이 “당신이 누구인지” 알 필요가 없습니다. 대신 서버가 신뢰할 수 있는 요청인지만 중요합니다.
그래서 사용자 인증(JWT) 대신 서버 인증(환경변수)을 사용하는 거죠.
마무리 통찰
7년간 일반 웹만 개발하다가 정적 블로그로 넘어오니 신선했습니다.
“로그인 없이 어떻게 보안을 유지하지?”라는 질문에 대한 답은 간단했습니다. 관점을 바꾸면 됩니다.
일반 웹에서는 “사용자가 누구인지”가 중요합니다. 하지만 공개 블로그에서는 “어떤 요청이 신뢰할 수 있는지”가 중요합니다. Netlify Functions는 그 신뢰를 환경변수로 보장해주고, 저는 서버 운영 부담 없이 백엔드 기능을 사용할 수 있게 됐습니다.
“왜 Netlify Functions를 써야 하나요?”
이제 이 질문에 자신있게 답할 수 있습니다. API 키를 안전하게 보호하면서 서버 운영 부담 없이 백엔드 기능을 사용하기 위해서입니다.
참고 :
https://docs.netlify.com/functions/overview/
https://developers.google.com/analytics/devguides/reporting/data/v1
https://supabase.com/docs/guides/api
