React Next.js 기반 Progressive Web App (PWA) 구축 실전 가이드
PWA의 핵심 개념부터 React, Next.js, TypeScript를 활용한 실제 구현 방법까지, 프론트엔드 개발자를 위한 PWA 구축 실전 가이드입니다. 서비스 워커, 웹 앱 매니페스트 설정 등을 다룹니다.
React Next.js 기반 Progressive Web App (PWA) 구축 실전 가이드
현대 웹 애플리케이션은 사용자에게 더 빠르고, 안정적이며, 몰입감 있는 경험을 제공해야 합니다. 이러한 요구사항을 충족시키는 강력한 기술 중 하나가 바로 Progressive Web App (PWA)입니다. PWA는 웹의 접근성과 앱의 사용자 경험을 결합하여, 오프라인 지원, 푸시 알림, 홈 화면 설치와 같은 기능을 제공함으로써 사용자 참여도를 극대화합니다.
이 글에서는 React와 Next.js를 기반으로 TypeScript 환경에서 PWA를 구축하는 실전적인 방법을 단계별로 안내합니다. PWA의 핵심 개념부터 next-pwa 라이브러리를 활용한 설정, Web App Manifest 구성, 그리고 Service Worker 구현까지, 실제 코드 예제와 함께 자세히 살펴보겠습니다.
PWA란 무엇인가요? 핵심 개념 이해하기
Progressive Web App (PWA)는 모바일 앱과 유사한 기능을 웹 환경에서 제공하는 웹 애플리케이션입니다. Google이 처음 제시한 개념으로, 웹 기술을 사용하여 앱과 같은 사용자 경험을 제공하는 것을 목표로 합니다. PWA의 핵심 특징은 다음과 같습니다.
- Reliable (안정성): 네트워크 연결이 불안정하거나 오프라인 상태에서도 콘텐츠를 즉시 로드할 수 있습니다. 이는 Service Worker 덕분입니다.
- Fast (빠른 속도): 정적인 콘텐츠를 캐싱하여 로딩 시간을 단축하고, 애니메이션 및 스크롤 경험을 부드럽게 만듭니다.
- Engaging (몰입감): 홈 화면에 설치 가능하며, 푸시 알림을 통해 사용자 재참여를 유도합니다. 전체 화면 모드(Fullscreen Mode)를 지원하여 앱과 같은 몰입감을 제공합니다.
이러한 특징을 구현하기 위한 세 가지 핵심 기술 요소는 다음과 같습니다.
- Service Worker: 백그라운드에서 실행되는 스크립트로, 네트워크 요청을 가로채고 캐싱을 관리하며 푸시 알림을 처리합니다. PWA의 오프라인 기능과 성능 향상에 필수적입니다.
- Web App Manifest: JSON 형식의 파일로, PWA의 이름, 아이콘, 시작 URL, 표시 방식(독립 실행형, 전체 화면 등), 테마 색상 등 메타데이터를 정의합니다. 이를 통해 브라우저는 PWA를 설치 가능한 앱처럼 취급할 수 있습니다.
- HTTPS: 모든 PWA는 보안을 위해 HTTPS를 통해 제공되어야 합니다. Service Worker는 네트워크 요청을 가로채기 때문에, 중간자 공격(Man-in-the-middle attack)을 방지하기 위해 HTTPS가 필수적입니다.
Next.js 프로젝트 초기 설정 및 next-pwa 통합
Next.js는 React 기반의 서버 사이드 렌더링(SSR) 및 정적 사이트 생성(SSG) 프레임워크로, PWA를 구축하기에 매우 적합합니다. next-pwa 라이브러리를 사용하면 Next.js 프로젝트에 PWA 기능을 손쉽게 통합할 수 있습니다.
1. Next.js 프로젝트 생성
아직 프로젝트가 없다면, 다음 명령어로 새로운 Next.js 프로젝트를 생성합니다. TypeScript 사용을 권장합니다.
npx create-next-app@latest my-pwa-app --typescript --eslint
cd my-pwa-app
2. next-pwa 라이브러리 설치
next-pwa는 Workbox를 기반으로 Service Worker를 자동으로 생성하고 관리해주는 유용한 라이브러리입니다.
npm install next-pwa
# 또는 yarn add next-pwa
3. next.config.js 설정
next.config.js 파일에 next-pwa 플러그인을 적용합니다.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
};
const withPWA = require('next-pwa')({
dest: 'public', // Service Worker 파일이 생성될 디렉토리
register: true, // Service Worker를 자동으로 등록할지 여부
skipWaiting: true, // Service Worker가 새 버전으로 업데이트될 때 즉시 활성화
disable: process.env.NODE_ENV === 'development', // 개발 환경에서는 PWA 비활성화
});
module.exports = withPWA(nextConfig);
dest: 'public'은 Service Worker 파일(sw.js, workbox-*.js 등)이 public 디렉토리에 생성되도록 지정합니다. disable: process.env.NODE_ENV === 'development'는 개발 중에는 PWA 기능을 비활성화하여 빌드 시간을 단축하고 디버깅을 용이하게 합니다.
Web App Manifest 설정하기
Web App Manifest는 PWA의 "앱 정보"를 담고 있는 JSON 파일입니다. 이 파일을 통해 브라우저와 운영체제는 웹 애플리케이션을 일반 앱처럼 인식하고 홈 화면에 추가할 수 있도록 합니다.
1. public/manifest.json 파일 생성
public 디렉토리 안에 manifest.json 파일을 생성하고 다음과 같이 내용을 작성합니다.
// public/manifest.json
{
"name": "My PWA App",
"short_name": "My PWA",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icons/icon-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/icons/icon-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"description": "My awesome Progressive Web App built with Next.js.",
"orientation": "portrait-primary"
}
-
name,short_name: 앱의 전체 이름과 짧은 이름을 정의합니다. -
icons: 다양한 크기의 앱 아이콘을 정의합니다.maskable아이콘은 Android 폰에서 다양한 모양으로 마스킹될 수 있도록 대비된 아이콘입니다. -
start_url: 앱이 시작될 때 로드될 URL입니다. 일반적으로 루트 경로(/)를 사용합니다. -
display: 앱이 표시되는 방식을 정의합니다.-
standalone: 브라우저 UI 없이 독립적인 앱처럼 실행됩니다. -
fullscreen: 상태 표시줄 없이 전체 화면으로 실행됩니다. -
minimal-ui: 브라우저 UI의 최소한만 표시됩니다. -
browser: 일반적인 브라우저 탭에서 실행됩니다.
-
-
theme_color: 앱의 UI 요소(예: 상태 표시줄)에 적용될 색상입니다. -
background_color: 앱이 로딩될 때 표시되는 스플래시 화면의 배경색입니다. -
description: 앱에 대한 간략한 설명입니다. -
orientation: 앱의 기본 화면 방향을 정의합니다.
아이콘 파일(public/icons/icon-*.png)을 반드시 준비하여 해당 경로에 배치해야 합니다.
2. _document.tsx 또는 _app.tsx에 Manifest 연결
Next.js 애플리케이션의 모든 페이지에서 Manifest 파일을 참조하도록 <head> 태그에 <link>를 추가해야 합니다. 일반적으로 pages/_document.tsx 파일에 추가하는 것이 권장됩니다.
// pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render() {
return (
<Html lang="ko">
<Head>
<meta name="application-name" content="My PWA App" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="My PWA App" />
<meta name="description" content="My awesome Progressive Web App" />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="msapplication-config" content="/icons/browserconfig.xml" /> {/* Windows Tile Icon */}
<meta name="msapplication-TileColor" content="#2B5797" />
<meta name="msapplication-tap-highlight" content="no" />
<meta name="theme-color" content="#000000" /> {/* Manifest theme_color와 일치 */}
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" /> {/* iOS Icon */}
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180x180.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/icons/icon-167x167.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="shortcut icon" href="/favicon.ico" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
여기에 추가된 meta 태그들은 iOS 장치와 같은 특정 플랫폼에서 PWA가 앱처럼 작동하도록 돕는 역할을 합니다. apple-touch-icon과 같은 아이콘들도 함께 준비하는 것이 좋습니다.
Service Worker 구현 및 캐싱 전략
Service Worker는 PWA의 핵심 중 하나로, 네트워크 요청을 가로채고, 리소스를 캐싱하며, 오프라인 지원을 가능하게 합니다. next-pwa 라이브러리는 Workbox를 사용하여 Service Worker를 자동으로 생성해주므로, 직접 Service Worker 코드를 작성할 필요 없이 next.config.js를 통해 캐싱 전략을 설정할 수 있습니다.
1. next-pwa의 기본 캐싱 전략
next-pwa는 기본적으로 Workbox의 다양한 캐싱 전략을 사용하여 리소스를 캐싱합니다.
- Pre-caching: 빌드 시점에 지정된 리소스(예:
_next/static의 JS, CSS 파일)를 Service Worker가 설치될 때 미리 캐싱합니다. 이는 앱의 핵심 쉘을 오프라인에서도 즉시 로드할 수 있게 합니다. - Runtime Caching: 사용자가 앱을 탐색하면서 요청하는 리소스(예: 이미지, API 응답)를 런타임에 캐싱합니다.
next-pwa는 다양한 캐싱 전략(StaleWhileRevalidate, CacheFirst, NetworkFirst 등)을 기본적으로 적용합니다.
2. 오프라인 폴백 페이지 설정
네트워크 연결이 없을 때 사용자에게 보여줄 오프라인 페이지를 설정할 수 있습니다. next-pwa는 offline.html 또는 pages/_offline.tsx를 인식합니다.
먼저 pages/_offline.tsx 파일을 생성합니다.
// pages/_offline.tsx
import React from 'react';
const OfflinePage: React.FC = () => {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
textAlign: 'center',
backgroundColor: '#f0f0f0',
color: '#333'
}}>
<h1>오프라인입니다. 😭</h1>
<p>인터넷 연결을 확인하고 다시 시도해주세요.</p>
<p>저장된 콘텐츠는 계속 이용할 수 있습니다.</p>
</div>
);
};
export default OfflinePage;
그리고 next.config.js에 fallback 옵션을 추가합니다.
// next.config.js
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
// 오프라인 폴백 페이지 설정
// Next.js의 경우 _offline.tsx를 사용하면 별도의 설정 없이 자동으로 처리됩니다.
// 만약 특정 HTML 파일을 사용하고 싶다면 아래와 같이 설정할 수 있습니다.
// fallback: {
// image: '/static/images/fallback.png',
// document: '/offline.html',
// audio: '/static/audios/fallback.mp3',
// video: '/static/videos/fallback.mp4',
// },
});
module.exports = withPWA(nextConfig);
next-pwa는 Next.js의 _offline.tsx 파일을 자동으로 감지하여 오프라인 폴백 페이지로 사용합니다. 따라서 별도의 fallback.document 설정을 하지 않아도 됩니다.
3. Service Worker 업데이트 처리
next-pwa는 skipWaiting: true 옵션을 통해 Service Worker가 새 버전으로 업데이트될 때 즉시 활성화되도록 설정할 수 있습니다. 이는 사용자가 페이지를 새로고침하지 않아도 최신 버전의 PWA를 경험하게 해줍니다.
만약 사용자에게 Service Worker 업데이트를 알리고, 사용자가 수동으로 업데이트를 적용하도록 하고 싶다면, next-pwa의 onUpdate 또는 onRegister 콜백을 활용하여 커스텀 UI를 구현할 수 있습니다.
// pages/_app.tsx 또는 특정 컴포넌트
import { useEffect } from 'react';
import type { AppProps } from 'next/app';
function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
if (typeof window !== 'undefined' && 'serviceWorker' in navigator && window.workbox !== undefined) {
// Workbox 인스턴스가 사용 가능한 경우
window.workbox.addEventListener('controlling', () => {
console.log('New Service Worker is controlling the page.');
// 새 버전이 활성화되었음을 사용자에게 알림
// alert('새로운 버전이 적용되었습니다. 페이지를 새로고침 해주세요.');
// window.location.reload();
});
window.workbox.addEventListener('waiting', (event) => {
console.log('New Service Worker is waiting to activate.', event);
// 사용자에게 업데이트가 있음을 알리고, '새로고침' 버튼을 클릭하도록 유도
if (confirm('새로운 버전이 있습니다! 지금 업데이트 하시겠습니까?')) {
window.workbox.messageSkipWaiting(); // 새 Service Worker 활성화
}
});
}
}, []);
return <Component {...pageProps} />;
}
export default MyApp;
위 코드는 Service Worker 업데이트가 감지되었을 때 사용자에게 알림을 띄우고, 업데이트를 수락하면 새 Service Worker를 활성화하는 예시입니다. 실제 프로덕션 환경에서는 사용자 경험을 고려하여 더 부드러운 UI를 제공하는 것이 좋습니다.
사용자 경험(UX) 최적화: Add to Home Screen (A2HS)
PWA의 가장 강력한 기능 중 하나는 사용자가 웹사이트를 스마트폰의 홈 화면에 앱처럼 설치할 수 있다는 점입니다. 이를 Add to Home Screen (A2HS) 기능이라고 합니다. 이 기능은 Web App Manifest와 Service Worker가 올바르게 설정되어 있을 때 활성화됩니다.
1. A2HS 프롬프트 조건
브라우저가 A2HS 프롬프트를 자동으로 띄우는 조건은 다음과 같습니다.
- 웹사이트가 HTTPS를 통해 제공되어야 합니다.
- 유효한 Web App Manifest 파일이 있어야 합니다.
- 유효한 Service Worker가 등록되어 있어야 합니다.
- 사용자가 웹사이트를 일정 시간 이상 사용하거나 방문한 이력이 있어야 합니다 (브라우저마다 다름).
2. 커스텀 A2HS 설치 버튼 구현
브라우저의 자동 프롬프트에 의존하기보다는, 사용자에게 명시적인 설치 버튼을 제공하여 A2HS 경험을 개선할 수 있습니다. beforeinstallprompt 이벤트를 활용하여 커스텀 설치 버튼을 구현하는 예제입니다.
// components/InstallPWAButton.tsx
import React, { useState, useEffect } from 'react';
interface BeforeInstallPromptEvent extends Event {
readonly platforms: Array<string>;
readonly userChoice: Promise<{
outcome: 'accepted' | 'dismissed';
platform: string;
}>;
prompt(): Promise<void>;
}
const InstallPWAButton: React.FC = () => {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
};
// PWA가 이미 설치되었는지 확인
if (window.matchMedia('(display-mode: standalone)').matches || (document as any).referrer.startsWith('android-app://')) {
setIsInstalled(true);
}
window.addEventListener('beforeinstallprompt', handler);
return () => {
window.removeEventListener('beforeinstallprompt', handler);
};
}, []);
const handleInstallClick = async () => {
if (!deferredPrompt) {
return;
}
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('사용자가 PWA 설치를 수락했습니다.');
setIsInstalled(true);
} else {
console.log('사용자가 PWA 설치를 거부했습니다.');
}
setDeferredPrompt(null);
};
if (isInstalled || !deferredPrompt) {
return null; // 이미 설치되었거나 설치 프롬프트가 없는 경우 버튼을 표시하지 않음
}
return (
<button
onClick={handleInstallClick}
style={{
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontSize: '16px',
marginTop: '20px'
}}
>
앱으로 설치하기
</button>
);
};
export default InstallPWAButton;
위 InstallPWAButton 컴포넌트를 pages/index.tsx 또는 pages/_app.tsx에 추가하여 사용합니다.
// pages/index.tsx
import Head from 'next/head';
import InstallPWAButton from '../components/InstallPWAButton';
export default function Home() {
return (
<div>
<Head>
<title>My PWA Home</title>
</Head>
<main style={{ textAlign: 'center', padding: '50px' }}>
<h1>환영합니다! PWA 앱입니다.</h1>
<p>이 웹사이트를 홈 화면에 추가하여 앱처럼 사용해보세요!</p>
<InstallPWAButton />
</main>
</div>
);
}
이 코드는 beforeinstallprompt 이벤트가 발생하면 deferredPrompt 상태에 이벤트를 저장하고, 이 상태가 있을 때만 "앱으로 설치하기" 버튼을 표시합니다. 사용자가 버튼을 클릭하면 deferredPrompt.prompt()를 호출하여 브라우저의 설치 프롬프트를 수동으로 트리거합니다.
PWA 배포 및 테스트
PWA를 성공적으로 구축했다면, 실제 환경에 배포하고 테스트하는 과정이 중요합니다.
1. 빌드 및 배포
PWA는 반드시 HTTPS 환경에서 제공되어야 합니다. Vercel, Netlify, AWS S3 + CloudFront 등 다양한 호스팅 서비스를 통해 Next.js 프로젝트를 배포할 수 있습니다.
npm run build
npm run start
npm run build 명령어를 실행하면 next-pwa가 public 디렉토리에 sw.js 및 관련 Workbox 파일을 생성합니다. 이 파일들이 배포 시 함께 업로드되는지 확인해야 합니다.
2. Lighthouse를 이용한 PWA 점수 측정
Chrome 개발자 도구의 Lighthouse 탭을 사용하여 PWA의 성능과 준수도를 측정할 수 있습니다.
- Chrome 브라우저에서 PWA URL로 접속합니다.
- 개발자 도구(F12)를 엽니다.
- "Lighthouse" 탭으로 이동합니다.
- "Categories"에서 "Progressive Web App"을 선택하고, 다른 필요한 카테고리(Performance, Accessibility 등)도 선택합니다.
- "Analyze page load" 버튼을 클릭합니다.
Lighthouse는 PWA의 모든 필수 기준(HTTPS, Manifest, Service Worker 등록, A2HS 가능성 등)을 검사하고 점수를 부여합니다. 이 점수를 통해 개선할 부분을 파악할 수 있습니다.
3. Chrome 개발자 도구의 Application 탭 활용
Chrome 개발자 도구의 "Application" 탭은 PWA 디버깅에 매우 유용합니다.
- Manifest: Manifest 파일의 내용을 확인하고 오류를 파악할 수 있습니다.
- Service Workers: 등록된 Service Worker의 상태를 확인하고, 업데이트, 등록 해제, 푸시 이벤트 시뮬레이션 등을 할 수 있습니다.
- Cache Storage: Service Worker가 캐싱한 리소스들을 확인할 수 있습니다.
- Storage: 로컬 스토리지, 세션 스토리지, IndexedDB 등을 관리할 수 있습니다.
이 도구들을 활용하여 PWA가 예상대로 작동하는지, 캐싱 전략이 올바르게 적용되었는지 등을 검증할 수 있습니다.
마무리
Progressive Web App (PWA)는 사용자에게 웹의 장점과 앱의 장점을 모두 제공하며, 현대 웹 개발에서 필수적인 요소로 자리매김하고 있습니다. React와 Next.js, 그리고 next-pwa 라이브러리를 활용하면 Service Worker와 Web App Manifest를 비교적 쉽게 통합하여 강력한 PWA를 구축할 수 있습니다.
이 가이드를 통해 PWA의 핵심 개념부터 실제 구현 방법까지 이해하고, 여러분의 웹 애플리케이션을 더욱 빠르고 안정적이며 몰입감 있는 경험으로 업그레이드할 수 있기를 바랍니다. PWA는 지속적으로 발전하고 있으며, 앞으로도 사용자에게 더 나은 웹 경험을 제공하는 데 중요한 역할을 할 것입니다.
관련 게시글
Vite Build Tool: Fast Frontend Development Guide
Vite는 현대적인 프론트엔드 개발을 위한 빠르고 효율적인 빌드 도구입니다. 이 가이드에서는 Vite의 핵심 기능, React 및 TypeScript 프로젝트 설정, 플러그인 활용법, 그리고 빌드 최적화 전략까지 완벽하게 다룹니다.
React Server Components (RSC) 심층 가이드: Next.js와 함께하는 Full-stack React
React Server Components (RSC)의 개념, 등장 배경, 동작 원리, 그리고 Next.js 13+ App Router에서의 활용법을 심층적으로 다룹니다. 클라이언트/서버 컴포넌트 분리 전략과 실전 코드 예제를 통해 RSC의 강력한 이점을 이해하고 웹 애플리케이션 성능을 최적화하는 방법을 알아봅니다.
Next.js Middleware: 강력한 요청 처리 활용법
Next.js Middleware를 활용하여 사용자 인증, 국제화, A/B 테스트 등 다양한 요청 처리 로직을 효율적으로 구현하는 방법을 심층적으로 알아봅니다. 실전 코드 예제를 통해 Next.js 애플리케이션의 프론트엔드 기능을 강화하세요.