Posts

[BUSGO] 리액트 PWA 프로젝트 생성

목차4

리액트 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"
  }

프로젝트 세팅 완료!

반응형