[DEVELOP] PWA 이해하기 (Progressive Web App)

React로 알아보는 PWA

Posted by lim.Chuck on February 25, 2025

[DEVELOP]

  1. [DEVELOP] DDD, TDD, BDD
  2. [DEVELOP] 개인정보 보호 웹사이트 구축을 위한
  3. [DEVELOP] 예제로 이해하는 웹 접근성 (accessibility)
  4. [DEVELOP] 예제로 보는 이미지 사용법 (Images)
  5. [DEVELOP] 예제로 보는 반응형 디자인 사용법 (Responsive Design)
  6. [DEVELOP] PWA 이해하기 (Progressive Web App)
  7. [DEVELOP] 개발 프로세스 Agile / Waterfall 이란?
  8. [DEVELOP] 주니어 개발자의 역습
  9. [DEVELOP] MCP(Model Context Protocol)
  10. [DEVELOP] MCP claude 적용하고 사용해보기

alt text

📱 1장: PWA(Progressive Web Apps) 시작하기

🤔 PWA가 뭔가요?

PWA는 Progressive Web Apps의 약자예요!

단어 의미 설명
Progressive (진보적) 점진적으로 발전해요 오래된 브라우저에서도 동작하고, 최신 브라우저에서는 더 많은 기능을 사용할 수 있어요
Web (웹) 웹사이트예요 인터넷 브라우저로 접속할 수 있어요
Apps (앱) 앱처럼 동작해요 스마트폰에 설치해서 사용할 수 있어요

쉽게 말하면…

“웹사이트인데 스마트폰 앱처럼 설치하고 사용할 수 있는 새로운 형태의 서비스예요!”

💡 왜 PWA가 생겼을까요?

기존 방식의 문제점 PWA의 해결방법
앱 설치가 귀찮아요 😫 웹사이트에서 바로 설치할 수 있어요!
앱이 너무 커요 😱 PWA는 매우 가벼워요 (보통 1-2MB)
데이터를 많이 써요 📶 한번 받으면 오프라인에서도 사용할 수 있어요
업데이트가 귀찮아요 🔄 자동으로 최신 버전이 유지돼요

⭐ PWA의 특별한 점

특징 설명 예시
📱 설치형 홈 화면에 설치할 수 있어요 크롬에서 “홈 화면에 추가” 클릭!
🌐 오프라인 인터넷이 없어도 돼요 지하철에서도 사용 가능!
🔔 푸시알림 새 소식을 알려줄 수 있어요 “새 메시지가 도착했어요!”
🚀 빠른 실행 매우 빠르게 실행돼요 1-2초 만에 실행!

🌟 실제 사용 예시

  1. 트위터 라이트
  • 일반 트위터 앱: 100MB+
  • PWA 버전: 1.5MB
  • 결과: 데이터 사용량 60% 감소!
  1. 스타벅스
  • 주문 앱을 PWA로 만듦
  • 오프라인에서도 메뉴 보기/주문 가능
  • 결과: 데스크톱 주문 2배 증가!

📱 누구에게 좋을까요?

이런 분들께 추천해요! 이유는…
📱 스마트폰 저장공간이 부족한 분 일반 앱의 1/10 크기예요
🌐 데이터가 제한적인 분 오프라인에서도 사용할 수 있어요
⚡ 빠른 실행이 필요한 분 설치 후 바로 실행돼요
🔄 자동 업데이트를 원하는 분 항상 최신 버전으로 유지돼요

💝 정리

  1. PWA는 웹사이트와 앱의 장점을 모두 가진 새로운 서비스예요
  2. 설치도 쉽고, 가볍고, 오프라인에서도 사용할 수 있어요
  3. 많은 기업들이 PWA를 도입하고 있어요
  4. 모바일 환경에서 점점 더 중요해질 거예요

📱 2장: React로 만드는 PWA

🔍 React PWA 시작하기

  1. CRA(Create React App)로 PWA 만들기
1
2
3
4
5
# PWA 템플릿으로 프로젝트 생성
npx create-react-app my-pwa --template cra-template-pwa

# 또는 기존 프로젝트에 PWA 추가
npm install workbox-webpack-plugin
  1. 기본 PWA 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// App.jsx
import React, { useState, useEffect } from "react";
import { Container, Button, Alert } from "@mui/material";

function App() {
  const [isInstallable, setIsInstallable] = useState(false);
  const [deferredPrompt, setDeferredPrompt] = useState(null);

  useEffect(() => {
    // PWA 설치 가능 여부 감지
    window.addEventListener("beforeinstallprompt", (e) => {
      e.preventDefault();
      setDeferredPrompt(e);
      setIsInstallable(true);
    });
  }, []);

  const handleInstall = async () => {
    if (deferredPrompt) {
      deferredPrompt.prompt();
      const { outcome } = await deferredPrompt.userChoice;
      if (outcome === "accepted") {
        console.log("PWA 설치 완료! 🎉");
      }
      setDeferredPrompt(null);
    }
  };

  return (
    <Container>
      <h1>나의 첫 React PWA! 👋</h1>
      {isInstallable && (
        <Button
          variant="contained"
          onClick={handleInstall}
          startIcon={<DownloadIcon />}
        >
          앱 설치하기 📱
        </Button>
      )}
    </Container>
  );
}

export default App;
  1. PWA 설정파일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// public/manifest.json
{
  "short_name": "React PWA",
  "name": "나의 첫 React PWA",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192",
      "purpose": "any maskable"
    },
    {
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

💡 주요 기능 구현하기

  1. 오프라인 상태 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// components/OfflineDetector.jsx
import React, { useState, useEffect } from "react";
import { Snackbar } from "@mui/material";

const OfflineDetector = () => {
  const [isOffline, setIsOffline] = useState(!navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOffline(false);
    const handleOffline = () => setIsOffline(true);

    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);

    return () => {
      window.removeEventListener("online", handleOnline);
      window.removeEventListener("offline", handleOffline);
    };
  }, []);

  return (
    <Snackbar
      open={isOffline}
      message="인터넷 연결이 끊겼어요! 😅"
      anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
    />
  );
};

export default OfflineDetector;
  1. 푸시 알림 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// hooks/usePushNotification.js
import { useState, useEffect } from "react";

export const usePushNotification = () => {
  const [permission, setPermission] = useState(Notification.permission);

  const requestPermission = async () => {
    try {
      const result = await Notification.requestPermission();
      setPermission(result);

      if (result === "granted") {
        // 서비스 워커 등록
        const registration = await navigator.serviceWorker.ready;
        // 푸시 구독
        const subscription = await registration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: "YOUR_VAPID_PUBLIC_KEY",
        });

        console.log("푸시 알림 구독 완료! 🔔", subscription);
      }
    } catch (error) {
      console.error("푸시 알림 에러:", error);
    }
  };

  return { permission, requestPermission };
};
  1. 데이터 캐싱 with React Query
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// hooks/useOfflineData.js
import { useQuery, useQueryClient } from "react-query";

export const useOfflineData = (key, fetchFn) => {
  const queryClient = useQueryClient();

  return useQuery(key, fetchFn, {
    // 오프라인일 때 캐시된 데이터 사용
    staleTime: Infinity,
    cacheTime: Infinity,
    onError: (error) => {
      if (!navigator.onLine) {
        // 오프라인일 때 캐시된 데이터 사용
        const cachedData = queryClient.getQueryData(key);
        if (cachedData) {
          return cachedData;
        }
      }
    },
  });
};

// 사용 예시
function PostList() {
  const { data, isLoading } = useOfflineData("posts", () =>
    fetch("/api/posts").then((res) => res.json())
  );

  if (isLoading) return <CircularProgress />;

  return (
    <List>
      {data?.map((post) => (
        <ListItem key={post.id}>{post.title}</ListItem>
      ))}
    </List>
  );
}

🎯 실제 사용 예시

완성된 PWA 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// components/PWAContainer.jsx
import React from "react";
import { Container, Paper } from "@mui/material";
import OfflineDetector from "./OfflineDetector";
import InstallPrompt from "./InstallPrompt";
import { usePushNotification } from "../hooks/usePushNotification";

const PWAContainer = ({ children }) => {
  const { permission, requestPermission } = usePushNotification();

  return (
    <Container>
      <Paper elevation={3} sx={{ p: 2, my: 2 }}>
        <InstallPrompt />
        {permission !== "granted" && (
          <Button onClick={requestPermission} startIcon={<NotificationsIcon />}>
            알림 허용하기 🔔
          </Button>
        )}
        {children}
      </Paper>
      <OfflineDetector />
    </Container>
  );
};

export default PWAContainer;

✨ 정리

  1. React로 PWA를 만들면 컴포넌트 기반으로 깔끔하게 구현할 수 있어요
  2. React Query로 오프라인 데이터 관리가 쉬워져요
  3. Custom Hook으로 PWA 기능을 재사용할 수 있어요
  4. Material-UI 같은 라이브러리로 예쁜 UI를 쉽게 만들 수 있어요

📱 3장: React PWA 고급 기능 다루기

1. 🚀 성능 최적화

Code Splitting과 Lazy Loading

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// App.jsx
import React, { Suspense, lazy } from "react";
import { CircularProgress } from "@mui/material";

// 컴포넌트 지연 로딩
const HomePage = lazy(() => import("./pages/HomePage"));
const ProfilePage = lazy(() => import("./pages/ProfilePage"));

function App() {
  return (
    <Suspense fallback={<CircularProgress />}>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/profile" element={<ProfilePage />} />
      </Routes>
    </Suspense>
  );
}

이미지 최적화

1
2
3
4
5
6
7
8
9
10
11
// components/OptimizedImage.jsx
import React from "react";

const OptimizedImage = ({ src, alt, ...props }) => {
  return (
    <picture>
      <source srcSet={src.replace(/\.(jpg|png)$/, ".webp")} type="image/webp" />
      <img src={src} alt={alt} loading="lazy" {...props} />
    </picture>
  );
};

2. 🔄 백그라운드 동기화

오프라인 데이터 동기화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// hooks/useOfflineSync.js
import { useState } from "react";
import { useMutation, useQueryClient } from "react-query";

export const useOfflineSync = () => {
  const [pendingActions, setPendingActions] = useState([]);
  const queryClient = useQueryClient();

  const syncMutation = useMutation(
    async (actions) => {
      for (const action of actions) {
        await fetch("/api/sync", {
          method: "POST",
          body: JSON.stringify(action),
        });
      }
    },
    {
      onSuccess: () => {
        setPendingActions([]);
        queryClient.invalidateQueries("userData");
      },
    }
  );

  // 오프라인 상태에서 액션 저장
  const addPendingAction = (action) => {
    setPendingActions((prev) => [...prev, action]);
  };

  // 온라인 상태가 되면 동기화
  useEffect(() => {
    const handleOnline = () => {
      if (pendingActions.length > 0) {
        syncMutation.mutate(pendingActions);
      }
    };

    window.addEventListener("online", handleOnline);
    return () => window.removeEventListener("online", handleOnline);
  }, [pendingActions]);

  return { addPendingAction, isPending: pendingActions.length > 0 };
};

3. 🔔 고급 푸시 알림

커스텀 푸시 알림

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// hooks/useCustomNotification.js
import { useEffect } from "react";

export const useCustomNotification = () => {
  const showCustomNotification = async ({ title, body, icon, actions }) => {
    const registration = await navigator.serviceWorker.ready;

    await registration.showNotification(title, {
      body,
      icon,
      badge: "/badge.png",
      vibrate: [200, 100, 200],
      actions: actions?.map((action) => ({
        action: action.id,
        title: action.title,
        icon: action.icon,
      })),
      data: {
        openUrl: window.location.origin,
      },
    });
  };

  // 알림 클릭 핸들링
  useEffect(() => {
    const handleNotificationClick = (event) => {
      event.notification.close();
      const url = event.notification.data.openUrl;
      clients.openWindow(url);
    };

    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.addEventListener(
        "notificationclick",
        handleNotificationClick
      );
    }

    return () => {
      navigator.serviceWorker.removeEventListener(
        "notificationclick",
        handleNotificationClick
      );
    };
  }, []);

  return { showCustomNotification };
};

4. 📱 앱 like 경험 제공

제스처 지원

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// components/SwipeableView.jsx
import React from "react";
import { useSwipeable } from "react-swipeable";

const SwipeableView = ({ children, onSwipe }) => {
  const handlers = useSwipeable({
    onSwipedLeft: () => onSwipe("left"),
    onSwipedRight: () => onSwipe("right"),
    preventDefaultTouchmoveEvent: true,
    trackMouse: true,
  });

  return <div {...handlers}>{children}</div>;
};

앱 상태 관리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// hooks/useAppState.js
import { useState, useEffect } from "react";

export const useAppState = () => {
  const [isActive, setIsActive] = useState(true);

  useEffect(() => {
    const handleVisibilityChange = () => {
      setIsActive(!document.hidden);
    };

    document.addEventListener("visibilitychange", handleVisibilityChange);
    return () => {
      document.removeEventListener("visibilitychange", handleVisibilityChange);
    };
  }, []);

  return isActive;
};

5. 🎨 실제 사용 예시

고급 기능이 적용된 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// components/AdvancedPWAFeatures.jsx
import React from "react";
import { Card, Button } from "@mui/material";
import { useOfflineSync } from "../hooks/useOfflineSync";
import { useCustomNotification } from "../hooks/useCustomNotification";
import { useAppState } from "../hooks/useAppState";

const AdvancedPWAFeatures = () => {
  const { addPendingAction, isPending } = useOfflineSync();
  const { showCustomNotification } = useCustomNotification();
  const isActive = useAppState();

  const handleAction = async () => {
    if (navigator.onLine) {
      // 온라인 동작
      await fetch("/api/action");
    } else {
      // 오프라인 동작
      addPendingAction({
        type: "ACTION",
        data: { timestamp: Date.now() },
      });
    }
  };

  const sendNotification = () => {
    showCustomNotification({
      title: "새로운 기능!",
      body: "고급 PWA 기능을 사용해보세요!",
      icon: "/logo192.png",
      actions: [
        { id: "explore", title: "살펴보기" },
        { id: "close", title: "닫기" },
      ],
    });
  };

  return (
    <Card sx={{ p: 2 }}>
      <h2>고급 PWA 기능</h2>
      {isPending && (
        <Alert severity="info">오프라인 작업이 대기 중입니다...</Alert>
      )}
      <Button onClick={handleAction}>작업 실행하기</Button>
      <Button onClick={sendNotification}>알림 보내기</Button>
      <div>앱 상태: {isActive ? "활성" : "비활성"}</div>
    </Card>
  );
};

✨ 정리

  1. Code Splitting으로 초기 로딩 속도를 개선할 수 있어요
  2. 오프라인 동기화로 끊김 없는 사용자 경험을 제공해요
  3. 커스텀 푸시 알림으로 사용자와 더 풍부하게 소통할 수 있어요
  4. 앱과 같은 제스처와 상태 관리로 네이티브한 경험을 제공해요

📱 4장: React PWA 배포와 모니터링

1. 🚀 배포 준비하기

빌드 최적화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// craco.config.js
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const WorkboxWebpackPlugin = require("workbox-webpack-plugin");

module.exports = {
  webpack: {
    configure: (webpackConfig) => {
      // 번들 크기 분석
      webpackConfig.plugins.push(
        new BundleAnalyzerPlugin({
          analyzerMode: "static",
          openAnalyzer: false,
        })
      );

      // PWA 캐싱 전략 설정
      webpackConfig.plugins.push(
        new WorkboxWebpackPlugin.InjectManifest({
          swSrc: "./src/service-worker.js",
          maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
        })
      );

      return webpackConfig;
    },
  },
};

환경 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/config/pwa.config.js
export const PWA_CONFIG = {
  // 개발 환경
  development: {
    apiUrl: "http://localhost:3000",
    cacheStrategy: "NetworkFirst",
    analyticsEnabled: false,
  },
  // 프로덕션 환경
  production: {
    apiUrl: "https://api.myapp.com",
    cacheStrategy: "CacheFirst",
    analyticsEnabled: true,
  },
};

2. 📊 성능 모니터링

성능 측정 훅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// hooks/usePerformanceMetrics.js
import { useEffect, useState } from "react";

export const usePerformanceMetrics = () => {
  const [metrics, setMetrics] = useState({
    fcp: 0, // First Contentful Paint
    lcp: 0, // Largest Contentful Paint
    fid: 0, // First Input Delay
    cls: 0, // Cumulative Layout Shift
  });

  useEffect(() => {
    // Web Vitals 측정
    const { getFCP, getLCP, getFID, getCLS } = require("web-vitals");

    getFCP((metric) => {
      setMetrics((prev) => ({ ...prev, fcp: metric.value }));
      sendToAnalytics("FCP", metric);
    });

    getLCP((metric) => {
      setMetrics((prev) => ({ ...prev, lcp: metric.value }));
      sendToAnalytics("LCP", metric);
    });

    getFID((metric) => {
      setMetrics((prev) => ({ ...prev, fid: metric.value }));
      sendToAnalytics("FID", metric);
    });

    getCLS((metric) => {
      setMetrics((prev) => ({ ...prev, cls: metric.value }));
      sendToAnalytics("CLS", metric);
    });
  }, []);

  return metrics;
};

성능 대시보드 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// components/PerformanceDashboard.jsx
import React from "react";
import { Card, LinearProgress, Typography } from "@mui/material";
import { usePerformanceMetrics } from "../hooks/usePerformanceMetrics";

const PerformanceDashboard = () => {
  const metrics = usePerformanceMetrics();

  const getScoreColor = (value, threshold) => {
    return value < threshold ? "success" : "error";
  };

  return (
    <Card sx={{ p: 3 }}>
      <Typography variant="h6">성능 지표 📊</Typography>

      <div>
        <Typography>
          First Contentful Paint (FCP)
          <LinearProgress
            variant="determinate"
            value={metrics.fcp}
            color={getScoreColor(metrics.fcp, 2000)}
          />
        </Typography>
      </div>

      {/* 다른 메트릭스도 비슷하게 표시 */}
    </Card>
  );
};

3. 🔍 에러 추적과 로깅

에러 바운더리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// components/ErrorBoundary.jsx
import React from "react";
import * as Sentry from "@sentry/react";

class PWAErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Sentry로 에러 보고
    Sentry.captureException(error, { extra: errorInfo });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>앗! 뭔가 잘못됐어요 😅</h2>
          <button onClick={() => window.location.reload()}>
            다시 시도하기
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

4. 📱 버전 관리와 업데이트

업데이트 감지 및 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// hooks/useAppUpdate.js
import { useState, useEffect } from "react";

export const useAppUpdate = () => {
  const [updateAvailable, setUpdateAvailable] = useState(false);

  useEffect(() => {
    // 서비스 워커 업데이트 감지
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.ready.then((registration) => {
        registration.addEventListener("updatefound", () => {
          const newWorker = registration.installing;

          newWorker.addEventListener("statechange", () => {
            if (
              newWorker.state === "installed" &&
              navigator.serviceWorker.controller
            ) {
              setUpdateAvailable(true);
            }
          });
        });
      });
    }
  }, []);

  const applyUpdate = () => {
    if (updateAvailable) {
      window.location.reload();
    }
  };

  return { updateAvailable, applyUpdate };
};

업데이트 알림 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// components/UpdateNotification.jsx
import React from "react";
import { Snackbar, Button } from "@mui/material";
import { useAppUpdate } from "../hooks/useAppUpdate";

const UpdateNotification = () => {
  const { updateAvailable, applyUpdate } = useAppUpdate();

  return (
    <Snackbar
      open={updateAvailable}
      message="새로운 버전이 있어요! 🆕"
      action={
        <Button color="secondary" size="small" onClick={applyUpdate}>
          업데이트하기
        </Button>
      }
    />
  );
};

5. 📈 사용 통계 수집

분석 훅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// hooks/useAnalytics.js
import { useEffect } from "react";
import { useLocation } from "react-router-dom";

export const useAnalytics = () => {
  const location = useLocation();

  useEffect(() => {
    // 페이지 뷰 추적
    trackPageView(location.pathname);

    // PWA 관련 이벤트 추적
    trackPWAEvents();
  }, [location]);

  const trackPWAEvents = () => {
    // 설치 추적
    window.addEventListener("appinstalled", () => {
      sendAnalytics("PWA", "installed");
    });

    // 오프라인 사용 추적
    window.addEventListener("offline", () => {
      sendAnalytics("PWA", "offline_usage");
    });
  };
};

✨ 정리

배포 체크리스트

  1. 빌드 최적화 확인
  2. 캐싱 전략 설정
  3. 성능 메트릭스 모니터링
  4. 에러 추적 시스템 구축
  5. 업데이트 메커니즘 구현

모니터링 포인트

  1. Core Web Vitals
  2. 오프라인 사용률
  3. 설치 전환율
  4. 에러 발생률
  5. 사용자 행동 패턴

📱 5장: PWA 성능 개선하기

1. 🎯 앱 성능을 높이는 방법

이미지 최적화하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// components/OptimizedImage.jsx
import React, { useState } from "react";

const OptimizedImage = ({ src, alt }) => {
  const [imageLoaded, setImageLoaded] = useState(false);

  // 💡 이미지가 잘 로드되었는지 확인해요
  const handleImageLoad = () => {
    console.log("✨ 이미지 로드 완료!");
    setImageLoaded(true);
  };

  return (
    <div style={{ position: "relative" }}>
      {/* 💡 이미지가 로드되기 전에 임시 이미지를 보여줘요 */}
      {!imageLoaded && (
        <div className="loading-placeholder">이미지 로딩중... 🌅</div>
      )}

      <img
        src={src}
        alt={alt}
        // 💡 필요할 때만 이미지를 불러와요
        loading="lazy"
        onLoad={handleImageLoad}
        style={{
          opacity: imageLoaded ? 1 : 0,
          transition: "opacity 0.3s ease-in-out",
        }}
      />
    </div>
  );
};

데이터 관리 최적화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// hooks/useDataOptimization.js
import { useState, useEffect } from "react";

export const useDataOptimization = () => {
  // 💡 데이터 저장소 상태를 관리해요
  const [storageInfo, setStorageInfo] = useState({
    used: 0,
    total: 0,
  });

  // 💡 저장소 정보를 가져와요
  const checkStorage = async () => {
    if ("storage" in navigator) {
      const info = await navigator.storage.estimate();
      console.log("📊 저장소 상태 체크중...");
      setStorageInfo({
        used: Math.round(info.usage / 1024 / 1024),
        total: Math.round(info.quota / 1024 / 1024),
      });
    }
  };

  // 💡 오래된 데이터를 정리해요
  const cleanOldData = async () => {
    console.log("🧹 오래된 데이터 정리중...");
    // 캐시 정리
    const caches = await window.caches.keys();
    for (let cache of caches) {
      await window.caches.delete(cache);
    }
    // 저장소 정보 업데이트
    checkStorage();
  };

  useEffect(() => {
    checkStorage();
  }, []);

  return { storageInfo, cleanOldData };
};

2. 🚀 앱 실행 속도 개선하기

필요한 것만 불러오기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// App.jsx
import React, { Suspense, lazy } from "react";

// 💡 필요할 때만 페이지를 불러와요
const HomePage = lazy(() => {
  console.log("🏠 홈 페이지 불러오는 중...");
  return import("./pages/HomePage");
});

const ProfilePage = lazy(() => {
  console.log("👤 프로필 페이지 불러오는 중...");
  return import("./pages/ProfilePage");
});

function App() {
  return (
    // 💡 페이지가 로드되는 동안 로딩 화면을 보여줘요
    <Suspense fallback={<div>로딩중... ⌛</div>}>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/profile" element={<ProfilePage />} />
      </Routes>
    </Suspense>
  );
}

성능 모니터링

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// components/PerformanceMonitor.jsx
import React, { useEffect, useState } from "react";

const PerformanceMonitor = () => {
  const [stats, setStats] = useState({
    loadTime: 0,
    memoryUsage: 0,
  });

  useEffect(() => {
    // 💡 페이지 로딩 시간을 측정해요
    const pageLoadTime =
      window.performance.timing.loadEventEnd -
      window.performance.timing.navigationStart;

    // 💡 메모리 사용량을 확인해요
    const memory = performance?.memory?.usedJSHeapSize || 0;

    console.log("📊 성능 체크 결과:", {
      로딩시간: `${pageLoadTime}ms`,
      메모리: `${Math.round(memory / 1024 / 1024)}MB`,
    });

    setStats({
      loadTime: pageLoadTime,
      memoryUsage: memory,
    });
  }, []);

  return (
    <div className="performance-stats">
      <h3>앱 상태 📊</h3>
      <p>페이지 로딩 시간: {stats.loadTime}ms</p>
      <p>메모리 사용량: {Math.round(stats.memoryUsage / 1024 / 1024)}MB</p>
    </div>
  );
};

3. 🔒 안전하게 데이터 저장하기

데이터 암호화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// utils/secureStorage.js
const SecureStorage = {
  // 💡 데이터를 안전하게 저장해요
  saveData: (key, data) => {
    try {
      // 데이터를 암호화해서 저장
      const encrypted = btoa(JSON.stringify(data));
      localStorage.setItem(key, encrypted);
      console.log("✅ 데이터 안전하게 저장 완료!");
    } catch (error) {
      console.error("❌ 데이터 저장 실패:", error);
    }
  },

  // 💡 저장된 데이터를 가져와요
  loadData: (key) => {
    try {
      const data = localStorage.getItem(key);
      if (!data) return null;
      // 암호화된 데이터를 해독
      return JSON.parse(atob(data));
    } catch (error) {
      console.error("❌ 데이터 불러오기 실패:", error);
      return null;
    }
  },
};

4. 🎨 최종 성능 개선된 앱

모든 기능을 합친 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// components/OptimizedApp.jsx
import React from "react";
import { useDataOptimization } from "../hooks/useDataOptimization";
import PerformanceMonitor from "./PerformanceMonitor";

const OptimizedApp = () => {
  const { storageInfo, cleanOldData } = useDataOptimization();

  return (
    <div className="app-container">
      <h1>우리 앱 📱</h1>

      {/* 성능 모니터링 */}
      <PerformanceMonitor />

      {/* 저장소 상태 */}
      <div className="storage-info">
        <h3>저장소 상태 💾</h3>
        <p>사용중: {storageInfo.used}MB</p>
        <p>전체 공간: {storageInfo.total}MB</p>
        <button onClick={cleanOldData}>오래된 데이터 정리하기 🧹</button>
      </div>

      {/* 이미지 최적화 예시 */}
      <div className="image-container">
        <h3>최적화된 이미지 🖼️</h3>
        <OptimizedImage src="/example.jpg" alt="예시 이미지" />
      </div>
    </div>
  );
};

✨ 이번 장에서 배운 내용

  1. 성능 개선 포인트
  • 이미지는 필요할 때만 불러오기
  • 데이터는 안전하게 저장하기
  • 오래된 데이터는 정리하기
  • 페이지는 필요할 때 불러오기
  1. 주요 기능
  • 이미지 최적화로 빠른 로딩
  • 안전한 데이터 저장
  • 성능 모니터링
  • 저장소 관리
  1. 사용자를 위한 개선점
  • 빠른 페이지 로딩
  • 안전한 데이터 보관
  • 효율적인 저장소 사용
  • 끊김 없는 사용자 경험

📱 6장: PWA 실제 서비스 적용하기

1. 🌟 실제 서비스 예시

트위터 라이트 스타일 앱 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// components/TwitterLikeApp.jsx
import React, { useState } from "react";
import { List, Avatar, TextField, Button } from "@mui/material";

const TwitterLikeApp = () => {
  const [posts, setPosts] = useState([]);
  const [newPost, setNewPost] = useState("");

  // 💡 오프라인에서도 작동하는 게시물 작성
  const handlePost = () => {
    const post = {
      id: Date.now(),
      text: newPost,
      timestamp: new Date().toLocaleString(),
      // 오프라인 상태 표시
      isOffline: !navigator.onLine,
    };

    setPosts([post, ...posts]);
    setNewPost("");

    // 오프라인일 때 로컬에 저장
    if (!navigator.onLine) {
      console.log("📱 오프라인 상태: 나중에 동기화할게요!");
      savePendingPost(post);
    }
  };

  return (
    <div className="twitter-like-app">
      <div className="post-form">
        <TextField
          fullWidth
          value={newPost}
          onChange={(e) => setNewPost(e.target.value)}
          placeholder="무슨 일이 일어나고 있나요?"
          multiline
        />
        <Button onClick={handlePost}>게시하기 ✨</Button>
      </div>

      <List>
        {posts.map((post) => (
          <div key={post.id} className="post">
            <Avatar>👤</Avatar>
            <div className="post-content">
              <p>{post.text}</p>
              <small>
                {post.timestamp}
                {post.isOffline && " (오프라인에서 작성됨)"}
              </small>
            </div>
          </div>
        ))}
      </List>
    </div>
  );
};

스타벅스 스타일 주문 앱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// components/CoffeeOrderApp.jsx
import React, { useState } from "react";
import { Card, Button } from "@mui/material";

const CoffeeOrderApp = () => {
  const [cart, setCart] = useState([]);
  const [orderStatus, setOrderStatus] = useState("ready");

  // 💡 메뉴 데이터 (실제로는 API에서 가져올 거예요)
  const menu = [
    { id: 1, name: "아메리카노", price: 4500 },
    { id: 2, name: "카페라떼", price: 5000 },
    { id: 3, name: "카푸치노", price: 5500 },
  ];

  // 💡 오프라인에서도 주문 가능하게 만들기
  const handleOrder = async () => {
    setOrderStatus("processing");

    try {
      if (navigator.onLine) {
        // 온라인: 서버에 주문 전송
        await submitOrder(cart);
        console.log("☕ 주문이 접수되었어요!");
      } else {
        // 오프라인: 로컬에 주문 저장
        saveOfflineOrder(cart);
        console.log("📱 오프라인 주문이 저장되었어요!");
      }

      setOrderStatus("completed");
      setCart([]);
    } catch (error) {
      console.error("주문 실패:", error);
      setOrderStatus("failed");
    }
  };

  return (
    <div className="coffee-order-app">
      <h2>메뉴판 ☕</h2>
      <div className="menu-list">
        {menu.map((item) => (
          <Card key={item.id} className="menu-item">
            <h3>{item.name}</h3>
            <p>{item.price}</p>
            <Button
              onClick={() => setCart([...cart, item])}
              variant="contained"
            >
              담기 🛒
            </Button>
          </Card>
        ))}
      </div>

      <div className="cart">
        <h3>장바구니 🛍️</h3>
        {cart.map((item) => (
          <div key={item.id}>
            {item.name} - {item.price}</div>
        ))}
        {cart.length > 0 && (
          <Button onClick={handleOrder} disabled={orderStatus === "processing"}>
            {orderStatus === "processing" ? "주문중..." : "주문하기 ✨"}
          </Button>
        )}
      </div>
    </div>
  );
};

2. 🔄 오프라인 동기화 구현하기

동기화 관리자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// utils/syncManager.js
class SyncManager {
  constructor() {
    this.pendingActions = [];
    this.setupSync();
  }

  // 💡 오프라인 동작 설정
  setupSync() {
    window.addEventListener("online", () => {
      console.log("🌐 인터넷 연결됨! 동기화를 시작할게요.");
      this.syncPendingActions();
    });

    window.addEventListener("offline", () => {
      console.log("📴 오프라인 모드로 전환되었어요.");
    });
  }

  // 💡 오프라인 작업 저장
  addPendingAction(action) {
    this.pendingActions.push(action);
    localStorage.setItem("pendingActions", JSON.stringify(this.pendingActions));
    console.log("✅ 오프라인 작업이 저장되었어요.");
  }

  // 💡 온라인 될 때 동기화
  async syncPendingActions() {
    const actions = this.pendingActions;
    if (actions.length === 0) return;

    console.log(`🔄 ${actions.length}개의 작업 동기화 중...`);

    for (const action of actions) {
      try {
        await this.processAction(action);
        console.log(`✅ 작업 동기화 성공: ${action.type}`);
      } catch (error) {
        console.error(`❌ 동기화 실패: ${action.type}`, error);
      }
    }

    this.pendingActions = [];
    localStorage.removeItem("pendingActions");
  }
}

3. 📱 실제 배포 준비하기

배포 체크리스트 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// components/DeploymentChecker.jsx
import React, { useEffect, useState } from "react";
import { List, ListItem, Checkbox } from "@mui/material";

const DeploymentChecker = () => {
  const [checks, setChecks] = useState({
    manifest: false,
    serviceWorker: false,
    https: false,
    responsive: false,
    offline: false,
  });

  // 💡 배포 전 체크사항 확인
  useEffect(() => {
    const runChecks = async () => {
      // manifest.json 체크
      const manifestCheck =
        document.querySelector('link[rel="manifest"]') !== null;

      // 서비스 워커 체크
      const swCheck = "serviceWorker" in navigator;

      // HTTPS 체크
      const httpsCheck = window.location.protocol === "https:";

      // 반응형 체크
      const responsiveCheck = window.matchMedia("(max-width: 768px)").matches;

      // 오프라인 기능 체크
      const cacheCheck = "caches" in window;

      setChecks({
        manifest: manifestCheck,
        serviceWorker: swCheck,
        https: httpsCheck,
        responsive: responsiveCheck,
        offline: cacheCheck,
      });
    };

    runChecks();
  }, []);

  return (
    <div className="deployment-checker">
      <h2>배포 전 체크리스트 ✅</h2>
      <List>
        <ListItem>
          <Checkbox checked={checks.manifest} readOnly />
          manifest.json 설정
        </ListItem>
        <ListItem>
          <Checkbox checked={checks.serviceWorker} readOnly />
          서비스 워커 등록
        </ListItem>
        <ListItem>
          <Checkbox checked={checks.https} readOnly />
          HTTPS 설정
        </ListItem>
        <ListItem>
          <Checkbox checked={checks.responsive} readOnly />
          반응형 디자인
        </ListItem>
        <ListItem>
          <Checkbox checked={checks.offline} readOnly />
          오프라인 지원
        </ListItem>
      </List>
    </div>
  );
};

✨ 정리

  1. 실제 서비스 구현 포인트
  • 오프라인 작동 지원
  • 데이터 동기화
  • 사용자 경험 최적화
  • 안정적인 동작
  1. 주요 기능
  • 게시물 작성/조회
  • 주문 시스템
  • 오프라인 동기화
  • 배포 전 체크리스트
  1. 실제 적용시 고려사항
  • 사용자 피드백 반영
  • 성능 모니터링
  • 오류 처리
  • 지속적인 업데이트

📱 7장: PWA 유지보수와 업데이트 관리

1. 🔄 자동 업데이트 시스템

업데이트 감지 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// components/UpdateDetector.jsx
import React, { useEffect, useState } from "react";
import { Snackbar, Button } from "@mui/material";

const UpdateDetector = () => {
  const [updateAvailable, setUpdateAvailable] = useState(false);

  useEffect(() => {
    // 💡 서비스 워커의 업데이트를 감지해요
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.ready.then((registration) => {
        registration.addEventListener("updatefound", () => {
          console.log("🔄 새로운 버전이 있어요!");
          setUpdateAvailable(true);
        });
      });
    }
  }, []);

  // 💡 새로운 버전으로 업데이트하기
  const handleUpdate = () => {
    console.log("✨ 새로운 버전으로 업데이트합니다!");
    window.location.reload();
  };

  return (
    <Snackbar
      open={updateAvailable}
      message="새로운 버전이 있어요! 🆕"
      action={
        <Button color="secondary" onClick={handleUpdate}>
          업데이트하기
        </Button>
      }
    />
  );
};

2. 📊 버전 관리 시스템

버전 관리 유틸리티

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// utils/versionManager.js
class VersionManager {
  constructor() {
    this.currentVersion = process.env.REACT_APP_VERSION || "1.0.0";
    this.setupVersionCheck();
  }

  // 💡 버전 정보를 확인해요
  setupVersionCheck() {
    console.log(`📱 현재 버전: ${this.currentVersion}`);

    // 로컬 저장소에서 이전 버전 확인
    const lastVersion = localStorage.getItem("appVersion");

    if (lastVersion !== this.currentVersion) {
      console.log("🆕 새로운 버전이 설치되었어요!");
      this.handleNewVersion();
    }
  }

  // 💡 새 버전 설치 후 필요한 작업을 처리해요
  handleNewVersion() {
    // 캐시 초기화
    if ("caches" in window) {
      caches.keys().then((names) => {
        names.forEach((name) => {
          caches.delete(name);
        });
      });
    }

    // 버전 정보 저장
    localStorage.setItem("appVersion", this.currentVersion);

    // 변경사항 안내
    this.showChangelog();
  }

  // 💡 변경사항을 보여줘요
  showChangelog() {
    console.log("✨ 이번 버전의 새로운 기능들이에요!");
    // 여기에 각 버전별 변경사항을 추가하세요
  }
}

3. 🛠️ 유지보수 도구

상태 모니터링 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// components/MaintenanceTools.jsx
import React, { useState, useEffect } from "react";
import { Card, Button, LinearProgress } from "@mui/material";

const MaintenanceTools = () => {
  const [stats, setStats] = useState({
    cacheSize: 0,
    lastUpdate: null,
    errors: [],
  });

  // 💡 앱 상태를 주기적으로 체크해요
  useEffect(() => {
    const checkAppHealth = async () => {
      // 캐시 크기 확인
      if ("storage" in navigator) {
        const estimate = await navigator.storage.estimate();
        const cacheSize = Math.round(estimate.usage / 1024 / 1024);

        setStats((prev) => ({
          ...prev,
          cacheSize,
          lastUpdate: new Date().toLocaleString(),
        }));
      }
    };

    // 1분마다 상태 체크
    const interval = setInterval(checkAppHealth, 60000);
    checkAppHealth(); // 초기 체크

    return () => clearInterval(interval);
  }, []);

  // 💡 문제가 생긴 부분을 고쳐요
  const handleMaintenance = async () => {
    console.log("🛠️ 유지보수를 시작합니다...");

    try {
      // 캐시 정리
      await caches
        .keys()
        .then((keys) => Promise.all(keys.map((key) => caches.delete(key))));

      // 로컬 데이터 정리
      const cleanupTasks = ["outdatedData", "tempFiles", "errorLogs"];

      cleanupTasks.forEach((task) => {
        localStorage.removeItem(task);
      });

      console.log("✨ 유지보수가 완료되었어요!");
    } catch (error) {
      console.error("유지보수 중 오류 발생:", error);
    }
  };

  return (
    <Card className="maintenance-tools">
      <h2>앱 관리 도구 🛠️</h2>

      <div className="stats">
        <p>캐시 크기: {stats.cacheSize}MB</p>
        <p>마지막 점검: {stats.lastUpdate}</p>
        <LinearProgress variant="determinate" value={stats.cacheSize / 100} />
      </div>

      <div className="actions">
        <Button variant="contained" onClick={handleMaintenance}>
          유지보수 실행하기 🧹
        </Button>
      </div>
    </Card>
  );
};

4. 📝 사용자 피드백 시스템

피드백 수집 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// components/FeedbackSystem.jsx
import React, { useState } from "react";
import { TextField, Button, Rating, Snackbar } from "@mui/material";

const FeedbackSystem = () => {
  const [feedback, setFeedback] = useState({
    rating: 0,
    comment: "",
    category: "general",
  });

  // 💡 사용자의 소중한 의견을 저장해요
  const handleSubmit = async () => {
    console.log("📝 피드백을 저장합니다...");

    try {
      // 오프라인일 때는 나중에 전송하도록 저장
      if (!navigator.onLine) {
        const pendingFeedbacks = JSON.parse(
          localStorage.getItem("pendingFeedbacks") || "[]"
        );

        pendingFeedbacks.push({
          ...feedback,
          timestamp: Date.now(),
        });

        localStorage.setItem(
          "pendingFeedbacks",
          JSON.stringify(pendingFeedbacks)
        );

        console.log("📱 오프라인 상태: 피드백을 임시 저장했어요!");
      } else {
        // 온라인일 때는 바로 전송
        await submitFeedback(feedback);
        console.log("✅ 피드백이 전송되었어요!");
      }

      // 입력 폼 초기화
      setFeedback({
        rating: 0,
        comment: "",
        category: "general",
      });
    } catch (error) {
      console.error("피드백 전송 실패:", error);
    }
  };

  return (
    <div className="feedback-system">
      <h2>여러분의 의견을 들려주세요! 💌</h2>

      <div className="rating-section">
        <p>앱이 마음에 드시나요?</p>
        <Rating
          value={feedback.rating}
          onChange={(_, value) =>
            setFeedback((prev) => ({ ...prev, rating: value }))
          }
        />
      </div>

      <TextField
        multiline
        rows={4}
        value={feedback.comment}
        onChange={(e) =>
          setFeedback((prev) => ({ ...prev, comment: e.target.value }))
        }
        placeholder="자유롭게 의견을 적어주세요!"
      />

      <Button
        variant="contained"
        onClick={handleSubmit}
        disabled={!feedback.comment || !feedback.rating}
      >
        의견 보내기 ✉️
      </Button>
    </div>
  );
};

✨ 정리

  1. 업데이트 관리
  • 자동 업데이트 감지
  • 버전 관리
  • 변경사항 안내
  • 원활한 업데이트 진행
  1. 유지보수 포인트
  • 정기적인 상태 체크
  • 캐시 관리
  • 오류 모니터링
  • 성능 최적화
  1. 사용자 소통
  • 피드백 수집
  • 오프라인 지원
  • 변경사항 안내
  • 사용자 경험 개선

📱 8장: PWA의 미래와 발전 방향

1. 🚀 최신 PWA 기술 트렌드

Project Fugu API 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// components/ModernPWAFeatures.jsx
import React, { useEffect, useState } from "react";
import { Card, Button } from "@mui/material";

const ModernPWAFeatures = () => {
  const [features, setFeatures] = useState({
    bluetooth: false,
    nfc: false,
    fileSystem: false,
    clipboard: false,
  });

  // 💡 최신 기능들을 확인해요
  useEffect(() => {
    const checkModernFeatures = async () => {
      console.log("🔍 최신 기능 지원 여부를 확인합니다...");

      // Bluetooth 지원 확인
      const bluetooth = "bluetooth" in navigator;

      // NFC 지원 확인
      const nfc = "nfc" in navigator;

      // 파일 시스템 접근 지원 확인
      const fileSystem = "showOpenFilePicker" in window;

      // 클립보드 API 지원 확인
      const clipboard = "clipboard" in navigator;

      setFeatures({
        bluetooth,
        nfc,
        fileSystem,
        clipboard,
      });
    };

    checkModernFeatures();
  }, []);

  return (
    <Card className="modern-features">
      <h2>최신 PWA 기능 🎉</h2>

      <div className="feature-list">
        <div className="feature-item">
          <h3>블루투스 연결 {features.bluetooth ? "" : ""}</h3>
          <p>주변 기기와 연결해보세요!</p>
        </div>

        <div className="feature-item">
          <h3>NFC 태그 {features.nfc ? "" : ""}</h3>
          <p>스마트 태그를 읽어보세요!</p>
        </div>

        <div className="feature-item">
          <h3>파일 시스템 {features.fileSystem ? "" : ""}</h3>
          <p>로컬 파일을 관리해보세요!</p>
        </div>

        <div className="feature-item">
          <h3>클립보드 접근 {features.clipboard ? "" : ""}</h3>
          <p>복사/붙여넣기를 활용해보세요!</p>
        </div>
      </div>
    </Card>
  );
};

2. 🎯 미래 대비 코드 작성

적응형 기능 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// components/FutureProofFeatures.jsx
import React from "react";
import { Card, Switch } from "@mui/material";

const FutureProofFeatures = () => {
  // 💡 브라우저 지원 여부를 확인해요
  const checkFeatureSupport = (featureName) => {
    switch (featureName) {
      case "share":
        return "share" in navigator;
      case "wakeLock":
        return "wakeLock" in navigator;
      case "badging":
        return "setAppBadge" in navigator;
      case "periodicSync":
        return "periodicSync" in navigator.serviceWorker;
      default:
        return false;
    }
  };

  // 💡 새로운 기능을 안전하게 사용해요
  const useFeature = async (featureName) => {
    console.log(`🚀 ${featureName} 기능을 사용해볼게요!`);

    try {
      switch (featureName) {
        case "share":
          if (checkFeatureSupport("share")) {
            await navigator.share({
              title: "멋진 PWA 앱",
              text: "새로운 기능을 체험해보세요!",
              url: window.location.href,
            });
          }
          break;

        case "wakeLock":
          if (checkFeatureSupport("wakeLock")) {
            await navigator.wakeLock.request("screen");
          }
          break;

        // 다른 기능들도 비슷하게 처리
      }
    } catch (error) {
      console.error(`${featureName} 기능 사용 중 오류:`, error);
    }
  };

  return (
    <Card className="future-proof">
      <h2>미래 대비 기능 🔮</h2>

      <div className="feature-toggles">
        <div className="feature-toggle">
          <span>공유하기 기능</span>
          <Switch
            checked={checkFeatureSupport("share")}
            onChange={() => useFeature("share")}
          />
        </div>

        <div className="feature-toggle">
          <span>화면 켜짐 유지</span>
          <Switch
            checked={checkFeatureSupport("wakeLock")}
            onChange={() => useFeature("wakeLock")}
          />
        </div>

        <div className="feature-toggle">
          <span>앱 배지</span>
          <Switch
            checked={checkFeatureSupport("badging")}
            onChange={() => useFeature("badging")}
          />
        </div>
      </div>
    </Card>
  );
};

3. 🌈 새로운 사용자 경험

AI 기능 통합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// components/AIFeatures.jsx
import React, { useState } from "react";
import { Card, TextField, Button } from "@mui/material";

const AIFeatures = () => {
  const [userInput, setUserInput] = useState("");
  const [aiResponse, setAiResponse] = useState("");

  // 💡 AI 기능을 PWA에 통합해요
  const handleAIInteraction = async () => {
    console.log("🤖 AI 기능을 실행합니다...");

    try {
      // 오프라인에서도 동작하는 기본 AI 기능
      if (!navigator.onLine) {
        const basicResponse = await offlineAIProcess(userInput);
        setAiResponse(basicResponse);
        return;
      }

      // 온라인에서는 고급 AI 기능 사용
      const response = await fetch("/api/ai", {
        method: "POST",
        body: JSON.stringify({ input: userInput }),
      });

      const data = await response.json();
      setAiResponse(data.response);
    } catch (error) {
      console.error("AI 처리 중 오류:", error);
      setAiResponse("죄송해요, 나중에 다시 시도해주세요 😅");
    }
  };

  return (
    <Card className="ai-features">
      <h2>AI 도우미 🤖</h2>

      <TextField
        fullWidth
        value={userInput}
        onChange={(e) => setUserInput(e.target.value)}
        placeholder="무엇을 도와드릴까요?"
      />

      <Button onClick={handleAIInteraction} variant="contained">
        AI에게 물어보기 ✨
      </Button>

      {aiResponse && (
        <div className="ai-response">
          <p>{aiResponse}</p>
        </div>
      )}
    </Card>
  );
};

4. 🎨 미래형 UI/UX

제스처 기반 인터페이스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// components/FutureUI.jsx
import React, { useEffect, useState } from "react";
import { motion } from "framer-motion";

const FutureUI = () => {
  const [gestureSupport, setGestureSupport] = useState(false);
  const [currentTheme, setCurrentTheme] = useState("light");

  // 💡 제스처 지원 여부를 확인해요
  useEffect(() => {
    const checkGestureSupport = () => {
      const support = "ongestureend" in window;
      setGestureSupport(support);
      console.log(`제스처 지원 ${support ? "가능" : "불가능"} 👋`);
    };

    checkGestureSupport();
  }, []);

  // 💡 제스처로 테마를 변경해요
  const handleGesture = (direction) => {
    console.log(`🎨 ${direction} 제스처 감지!`);

    switch (direction) {
      case "up":
        setCurrentTheme("light");
        break;
      case "down":
        setCurrentTheme("dark");
        break;
      // 다른 제스처들도 처리
    }
  };

  return (
    <motion.div
      className={`future-ui ${currentTheme}`}
      onPanEnd={(e, info) => {
        const direction = info.offset.y > 0 ? "down" : "up";
        handleGesture(direction);
      }}
    >
      <h2>미래형 인터페이스 ✨</h2>

      <div className="gesture-area">
        <p>
          {gestureSupport
            ? "여기에서 제스처를 사용해보세요!"
            : "제스처가 지원되지 않아요 😅"}
        </p>
      </div>

      <div className="theme-preview">
        <p>현재 테마: {currentTheme}</p>
        <small>위/아래로 스와이프해서 테마를 바꿔보세요!</small>
      </div>
    </motion.div>
  );
};

✨ 정리

  1. 새로운 기술 트렌드
  • Project Fugu API
  • AI 통합
  • 제스처 인터페이스
  • 적응형 기능
  1. 미래 대비 포인트
  • 브라우저 지원 확인
  • 대체 기능 준비
  • 점진적 기능 향상
  • 사용자 경험 개선
  1. PWA의 발전 방향
  • 네이티브 앱 수준의 기능
  • AI 기반 개인화
  • 직관적인 인터페이스
  • 더 나은 오프라인 경험

이렇게 해서 PWA의 현재와 미래에 대해 알아보았어요! PWA는 계속해서 발전하고 있으며, 웹의 미래를 이끌어갈 중요한 기술이 될 거예요. 😊