RedwoodJS는 React 기반의 풀스택 프레임워크로, GraphQL과 결합하여 강력한 서버사이드 및 정적 생성 기능을 제공한다. 본 문서에서는 RedwoodJS의 렌더링 메커니즘, Cell 패턴, 그리고 실전 최적화 전략을 심층적으로 다룬다.
렌더링 아키텍처 이해
RedwoodJS는 세 가지 렌더링 전략을 제공한다. 각 전략은 특정 사용 사례에 최적화되어 있으며, 개발자는 페이지별로 적절한 방식을 선택할 수 있다.
| 모드 | 실행 시점 | 적합한 상황 |
|---|---|---|
| CSR (Client-Side Rendering) | 브라우저 런타임 | 대시보드, 관리자 패널 |
| SSR (Server-Side Rendering) | 요청 시 서버에서 | SEO 중시 콘텐츠 |
| SSG (Static Site Generation) | 빌드 시점 | 마케팅 페이지, 문서 |
Cell 패턴: 선언적 데이터 페칭
RedwoodJS의 독창적인 Cell 패턴은 데이터 요청의 라이프사이클을 선언적으로 관리한다. 단일 파일 내에서 로딩, 성공, 실패, 빈 상태를 모두 정의한다.
// src/components/ArticleCell/ArticleCell.jsx
export const FETCH_ARTICLE = gql`
query FetchArticle($slug: String!) {
article: postBySlug(slug: $slug) {
headline
body
publishedAt
author {
fullName
avatarUrl
}
}
}
`
export const Pending = () => (
<Skeleton variant="article" rows={5} />
)
export const Rejected = ({ error }) => (
<ErrorBoundary
title="콘텐츠를 불러올 수 없습니다"
detail={error.message}
retryable
/>
)
export const Empty = () => (
<NotFound
resource="게시글"
suggestion="다른 키워드로 검색해보세요"
/>
)
export const Fulfilled = ({ article }) => (
<article className="prose lg:prose-xl">
<header>
<h1>{article.headline}</h1>
<AuthorProfile author={article.author} date={article.publishedAt} />
</header>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
</article>
)
레이아웃 구성과 중첩 라우팅
RedwoodJS는 파일 시스템 기반 라우팅과 레이아웃 상속을 지원한다. 중첩된 레이아웃을 통해 UI 일관성을 유지하면서도 유연한 페이지 구성이 가능하다.
// src/layouts/AppLayout/AppLayout.jsx
import { MetaTags } from '@redwoodjs/web'
const AppLayout = ({ children, breadcrumbs }) => {
const { currentUser, logOut } = useAuth()
return (
<div className="app-container">
<MetaTags titleTemplate="%s | MyApp" />
<TopNavigation
user={currentUser}
onSignOut={logOut}
items={navigationItems}
/>
<aside className="sidebar">
<BreadcrumbTrail items={breadcrumbs} />
<SecondaryMenu />
</aside>
<main className="content-area">
{children}
</main>
<ToastContainer position="bottom-right" />
</div>
)
}
export default AppLayout
동적 세그먼트와 정적 생성
동적 라우트에 대한 정적 페이지를 생성하려면 getStaticPaths 함수를 활용한다. 빌드 시점에 모든 가능한 경로를 미리 생성하여 CDN 배포에 최적화한다.
// src/pages/[category]/[productId].jsx
import { ProductCell } from 'src/components/ProductCell'
export const getStaticPaths = async () => {
const categories = await db.category.findMany({
include: { products: { select: { slug: true } } }
})
const paths = categories.flatMap(cat =>
cat.products.map(prod => ({
params: {
category: cat.handle,
productId: prod.slug
}
}))
)
return { paths, fallback: 'blocking' }
}
const ProductDetailPage = ({ category, productId }) => {
return (
<ProductCell
categoryHandle={category}
productSlug={productId}
/>
)
}
export default ProductDetailPage
성능 최적화 패턴
코드 분할과 지연 로딩
import { lazy, Suspense } from 'react'
const HeavyChart = lazy(() => import('src/components/AnalyticsChart'))
const DashboardPage = () => {
return (
<div className="dashboard">
<KpiCards />
<Suspense fallback={<ChartPlaceholder />}>
<HeavyChart dateRange={last30Days} />
</Suspense>
</div>
)
}
이미지 최적화 구성
// redwood.toml
[web]
port = 8910
apiUrl = "/.netlify/functions"
[experimental]
streamingSsr = true
[images]
domains = ["cdn.example.com", "images.unsplash.com"]
sizes = [640, 1280, 1920]
formats = ["webp", "avif"]
테스트 전략
RedwoodJS는 Jest와 Testing Library를 기본으로 제공한다. Cell 컴포넌트의 각 상태를 개별적으로 테스트한다.
// ArticleCell.test.jsx
import { render, screen } from '@redwoodjs/testing/web'
import { Pending, Rejected, Fulfilled } from './ArticleCell'
describe('ArticleCell 상태', () => {
it('로딩 중 스켈레톤을 표시한다', () => {
render(<Pending />)
expect(screen.getByRole('progressbar')).toBeInTheDocument()
})
it('에러 발생 시 재시도 버튼을 제공한다', () => {
const networkError = new Error('ECONNREFUSED')
render(<Rejected error={networkError} />)
expect(screen.getByRole('button', { name: /다시 시도/i })).toBeEnabled()
})
it('정상 응답 시 콘텐츠를 렌더링한다', () => {
const mockArticle = {
headline: 'RedwoodJS 심화',
body: '<p>상세 내용</p>',
author: { fullName: '김개발', avatarUrl: null }
}
render(<Fulfilled article={mockArticle} />)
expect(screen.getByText('RedwoodJS 심화')).toBeInTheDocument()
})
})
배포 구성 예시
Netlify, Vercel, Render 등 다양한 플랫폼에 최적화된 설정을 제공한다. Edge Function을 활용한 지역별 캐싱 전략도 지원한다.
// netlify.toml
[build]
command = "yarn rw build"
publish = "web/dist"
[[edge_functions]]
function = "geo-redirect"
path = "/api/*"
[context.production.environment]
NODE_ENV = "production"
REDWOOD_API_URL = "/.netlify/functions"