목차
반응형
리액트 PWA 프로젝트 생성
이번 서비스의 목표는 어플로까지 발행해서 주변 지인들이 앱으로 편하게 사용할 수 있도록 하는 것이다.
그래서 센텐스유의 PWA템플릿을 그대로 가져와서 리액트 프로젝트를 생성했다.
빌드는 웹팩 바벨 기반이고, 추가로 설치한 라이브러리들의 목록은 아래와 같다.
- axios : 공공데이터 API 통신용
- dayjs : 날짜 관련 라이브러리
- emotion : 스타일용
- pwa
- react-query : 서버데이터 전역관리용
- sweetalert2 : alert 알림용
- socket.io : 실시간 버스 통신용
service-worker.jsx 전체코드
/* eslint-disable no-restricted-globals */
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute } from 'workbox-precaching';
import { offlineFallback } from 'workbox-recipes';
import { NavigationRoute, Route, registerRoute, setDefaultHandler } from 'workbox-routing';
import { CacheFirst, NetworkFirst, NetworkOnly, StaleWhileRevalidate } from 'workbox-strategies';
clientsClaim();
precacheAndRoute(self.__WB_MANIFEST || []); // 없으면 빌드 시 오류(공식문서)
/*===================================================
오프라인 캐싱 및 SW 설치
===================================================*/
const FALLBACK_CACHE_NAME = 'offline-fallback';
const FALLBACK_HTML = '../offline.html';
const networkWithFallbackStrategy = new NetworkOnly({
// 오프라인이거나 네트워크 응답이 있기 전에
// 5초 이상 경과한 경우 캐시된 offline.html로 폴백
networkTimeoutSeconds: 5,
plugins: [
{
handlerDidError: async () => {
return await caches.match(FALLBACK_HTML, {
cacheName: FALLBACK_CACHE_NAME,
});
},
},
],
});
setDefaultHandler(new NetworkOnly());
offlineFallback();
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(FALLBACK_CACHE_NAME).then((cache) => cache.add(FALLBACK_HTML)));
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)));
}),
);
console.log('install:: SW 설치 완료');
console.log('install:: SW 캐시 삭제');
});
/*===================================================
여러 항목 캐싱
===================================================*/
// 이미지 캐싱:
const imageRoute = new Route(
({ request }) => {
return request.destination === 'image';
},
new StaleWhileRevalidate({
cacheName: 'images',
}),
);
// 스크립트 캐싱:
const scriptsRoute = new Route(
({ request }) => {
return request.destination === 'script';
},
new CacheFirst({
cacheName: 'scripts',
}),
);
// 스타일 캐싱:
const stylesRoute = new Route(
({ request }) => {
return request.destination === 'style';
},
new CacheFirst({
cacheName: 'styles',
}),
);
// 라우트 등록
registerRoute(imageRoute);
registerRoute(scriptsRoute);
registerRoute(stylesRoute);
/*===================================================
불필요한 캐시 항목 제거
===================================================*/
const imageExpRoute = new Route(
({ request }) => {
return request.destination === 'image';
},
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
// 30일 이상 지난 이미지 캐시 항목 제거
maxAgeSeconds: 60 * 60 * 24 * 30,
}),
],
}),
);
const scriptsExpRoute = new Route(
({ request }) => {
return request.destination === 'script';
},
new CacheFirst({
cacheName: 'scripts',
plugins: [
new ExpirationPlugin({
// 캐시에 50개 이상의 항목이 있는 경우
// 가장 적게 사용되는 스크립트 캐시 항목을 제거
maxEntries: 50,
}),
],
}),
);
// 라우트 등록
registerRoute(imageExpRoute);
registerRoute(scriptsExpRoute);
/*===================================================
사전 캐싱 없이 Workbox 사용
===================================================*/
const navigationRoute = new NavigationRoute(
new NetworkFirst({
cacheName: 'navigations',
}),
);
const imageAssetRoute = new Route(
({ request }) => {
return request.destination === 'image';
},
new CacheFirst({
cacheName: 'image-assets',
}),
);
// 라우터 등록:
registerRoute(navigationRoute);
registerRoute(imageAssetRoute);
// 모든 탐색을 처리할 경로를 등록
registerRoute(new NavigationRoute(networkWithFallbackStrategy));
/*============================================
새로운 버전의 SW 업데이트 수락 시
============================================*/
addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// // SW 버전 관리
// const SW_VERSION = '1.0.0';
// self.addEventListener('message', (event) => {
// if (event.data.type === 'GET_VERSION') {
// event.ports[0].postMessage(SW_VERSION);
// }
// });
client.jsx 전체코드
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import axios from 'axios';
import { Workbox } from 'workbox-window';
import App from './App';
import GlobalStyle from '@styles/global';
const isDevelopment = process.env.NODE_ENV !== 'production';
const root = ReactDOM.createRoot(document.getElementById('root'));
/* Axios 기본 설정 */
axios.defaults.withCredentials = true;
if (isDevelopment) {
axios.defaults.baseURL = 'http://localhost:8000'; // 개발
} else {
axios.defaults.baseURL = '배포url'; // 배포
}
/* React-Query 기본 설정 */
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // window focus 설정
},
},
});
/* PWA */
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
/*===================================================
SW 등록
===================================================*/
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
registration.unregister(); // 기존 SW 해제
console.log('register:: SW 등록 성공');
})
.catch((error) => {
console.log('SW 등록 실패, Error:: ', error);
});
/*===================================================
Work Box 사용
===================================================*/
const wb = new Workbox('/sw.js');
wb.addEventListener('installed', (event) => {
if (!event.isUpdate) {
console.log('installed:: Workbox 설치 완료');
}
});
wb.register();
// 자동 업데이트
wb.messageSkipWaiting();
});
}
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<GlobalStyle />
<App />
</BrowserRouter>
<ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider>
</React.StrictMode>,
);
package.json 의존성 목록
"dependencies": {
"@babel/runtime-corejs3": "^7.20.13",
"@emotion/core": "^11.0.0",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@loadable/component": "^5.15.2",
"axios": "^1.2.2",
"cross-env": "^7.0.3",
"dayjs": "^1.11.7",
"emotion-normalize": "^11.0.1",
"react": "^18.2.0",
"react-icons": "^4.7.1",
"react-ios-pwa-prompt": "^1.8.4",
"react-router-dom": "^6.6.1",
"socket.io-client": "^4.5.4",
"sweetalert2": "^11.7.1",
"web-push": "^3.5.0",
"workbox-window": "^6.5.4"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@emotion/babel-plugin": "^11.10.5",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@stylelint/postcss-css-in-js": "^0.38.0",
"@tanstack/react-query": "^4.22.4",
"@tanstack/react-query-devtools": "^4.22.4",
"babel-loader": "^8.3.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"dotenv": "^16.0.3",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-config-react-app": "^7.0.1",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"husky": "^8.0.3",
"lint-staged": "^13.1.2",
"postcss": "^8.4.21",
"postcss-syntax": "^0.36.2",
"process": "^0.11.10",
"react-refresh": "^0.14.0",
"stylelint": "^15.1.0",
"stylelint-config-concentric-order": "^5.1.0",
"stylelint-config-recommended": "^10.0.1",
"stylelint-order": "^6.0.2",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.7.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-merge": "^5.8.0",
"webpack-pwa-manifest": "^4.3.0",
"workbox-cli": "^6.5.4",
"workbox-webpack-plugin": "^6.5.4"
}
프로젝트 세팅 완료!
반응형