✅ 네이티브 앱이 뭔가요?
👉 앱스토어나 구글 플레이스토어에서 다운받아서 설치하는 앱
ex. 카톡, 배민,…
장단점
빠르고 성능 좋음 | 설치 필수! |
카메라, GPS, 알림 등의 기능 사용 가능 | 앱 업데이트 귀찮음 |
앱스토어에 등록되어 있어서 신뢰성 높음 | 개발 비용이 많이 들기도 함 |
✅ PWA란?
👉 프로그레시브 웹 앱(Progressive Web Apps)의 줄임말로, 모바일 기기에서 네이티브 앱과 같은 사용자 경험을 제공하는 웹 앱
👉 웹사이트를 앱처럼 사용할 수 있게 만드는 기술
👉 웹에서 사용하는 기술과 네이티브 앱의 장점을 결합한 것.
👉 설치 없이 바로 사용할 수 있는 앱
👉 특징
- APP 같다 - 실제 앱처럼 홈화면에 앱 아이콘을 설치하여 쉽게 바로가기가 가능하다.
- push 메시지 기능
- 원래 웹은 클라이언트에서 서버로 요청이 있어야만 결과물을 보내주는 형태로 구현된다. push는 반대로 클라이언트의 요청이 없더라도 서버의 필요에 의해서 클라이언트에게 데이터를 보낼 수 있는 기능이다. PWA에서는 push도 가능하다!
- offline 접속 기능
장단점
설치가 필요 없음 | 성능이 네이티브 앱보다 살짝 느림 |
개발 비용 저럼 | 아이폰에서 알림 같은 기능 제한됨 |
업데이트 자동 | 고사양 기능(블루투스, AR) 사용이 어려움 |
수익관련 문제로 아직 PWA 거부하는 곳도 있음 |
👉 사용 방법
- 모바일에서 웹 사이트 들어가기
- “홈 화면에 추가” 버튼을 누르면 끝
- 아이콘이 앱처럼 핸드폰에 생김
👉 (정리_)기존의 전통적인 웹 앱과 뭐가 다른가?
- 네이티브 앱과 유사한 기능을 제공
- PWA를 사용하면 사용자가 앱을 다운로드하거나, 업데이트할 필요 없이 웹 브라우저를 통해 앱을 바로 사용할 수 있다.
- 웹 페이지와 달리 오프라인에서도 작동 가능
- 웹 페이지가 로딩되는 동안 오프라인에서 캐시된 데이터를 사용할 수 있고, 네트워크 연결이 되면 새로운 데이터를 불러와 업데이트 할 수 있다.
- 네이티브 앱과 마찬가지로 푸시 알림 기능 제공
- 카메라, 마이크 등 모바일 기기 자체의 기능도 사용 가능
✅ PWA를 알아야 하는 이유
굳이 네이티브 앱을 두고 왜 PWA를 학습하고 적용해야 하는가?
- 네이티브 앱 수준의 모바일 친화적 웹 개발 가능
- 모바일 환경에서의 웹은 사용자에게 푸시 알림을 보내거나, 오프라인 상태에서 동작하는 기능 자체를 제공하지 않음
- 네이티브 앱을 공부하지 않아도 동등한 수준으로 개발 가능
- 네이티브 앱 개발을 학습하는데 비해 훨씬 낮은 학습량을 가짐. 네이티브 앱의 경우 처음부터 다시 학습해야 하고, 보통 OS마다 다른 방식으로 개발하기 때문에 이에 따른 학습량이 더 많음. 하지만 PWA의 경우 한 번의 개발로 안드로이드,iOS 모두 호환 가능
- 기업에서 바라보는 PWA
- 기업의 입장에서는 프론트엔드 개발자와 네이티브 앱 개발자를 따로 채용해 서비스를 구성하는 것보다, PWA를 아는 프론트엔드 개발자만 채용하는 것이 상대적으로 비용 절감이 되기 때문에 PWA를 잘 아는 프론트엔드 개발자에 대한 기업 수료가 증가할 것으로 예상할 수 있다.
✅ PWA 구현을 위한 기술적 요소
서비스 워커
- 백그라운드에서 실행되는 스크립트
- 웹 앱의 작업을 보조하고 추가 기능을 제공하는 역할
- 네트워크 요청을 가로채고 캐싱하는 역할
- 왜냐? 서비스 워커를 통해 오프라인 지원, 데이터 캐싱, 푸시 알림 등의 기능 구현 가능
- 주요 기능
- 오프라인 기능 제공: 인터넷 연결이 끊겼을 때오 중요한 파일들을 미리 캐시해두면, 캐시된 자원으로 웹 앱 사용 가능
- 푸시 알림: 사용자가 웹 페이지를 떠나더라도 알림을 보낼 수 있음
- 백그라운드 데이터 동기화 : 앱을 사용하지 않더라도 백그라운드에서 데이터를 동기화하거나 서버와의 통신 처리 가능
- ex. 사용자가 오프라인 상태에서 작성한 데이터가 다시 온라인 상태가 되면 서버와 자동으로 동기화될 수 있음
- 캐시 관리: 특정 리소스를 미리 저장해두고, 사용자가 요청할 때 빠르게 제공 가능
웹 앱 매니페스트
- 웹 애플리케이션에 대한 메타데이터를 제공하는 JSON 파일
- 애플리케이션을 홈 화면에 추가할 때의 아이콘, 이름, 시작 URL 등을 정의
애플리케이션 셸 아키텍쳐
- 사용자 인터페이스의 기본 구조를 미리 로딩
- ⇒ 콘텐츠가 동적으로 로드 되는 동안에도 사용자에게 빠른 경험 제공
✅ 서비스 워커 동작 방식 예시 코드
manifest.json 설정하기
먼저 public/manifest.json 파일
- 웹 애플리케이션의 메타데이터를 포함하고 있다.
- 이 파일을 통해 브라우저가 앱을 어떻게 표시하고 동작하게 할지 결정한다.
// 웹 앱의 이름, 아이콘, 배경 색상 등 기본 정보를 지정.
// 웹 앱을 설치했을 떄의 모습과 동작 방식을 정의
{
"short_name": "React App",
// 홈화면에 아이콘을 추가했을 때 React App이라는 이름이 표시됨
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
// standalone은 앱을 네이티브 앱처럼 화면 전체로 표시하도록 설정
// 주소창이나 다른 브라우저 UI 요소 없이 전체 화면에 표시됨.
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
service worker 설정하기
가장 주용한 흐름 : 설치(install) - 활성화(activate) - 요청처리(fetch)
Service Worker는 웹 애플리케이션을 백그라운드에서 실행할 수 있게 해주는 스크립트 ⇒ 인터넷 연결 없이도 앱을 사용할 수 있게 하거나, 빠르게 로딩하도록 함.
service-worker.ts에서는 Workbox 라이브러리를 사용하여 캐싱, 백그라운드 동기화, 푸시 알림 등의 작업 처리
오프라인 지원: Service Worker를 사용하여 웹 앱의 콘텐츠를 캐싱하여 오프라인 상태에서도 사용자가 앱을 이용할 수 있도록 합니다. 캐싱된 리소스를 이용하여 기본적인 사용자 경험을 제공하거나 일부 기능을 오프라인에서도 사용 가능하게 합니다.
//service-worker.ts
// 웹 앱의 Service Worker를 설정하고 관리하는 부분(타입스크립트 파일)
// 웹 애플리케이션이 PWA로 동작할 수 있도록 함.
// 이 파일에서 주로 사용되는 라이브러리는 workbox 라이브러리인데, 이 라이브러리는 서비스 워커의 구현을 단순화시켜주고, 다양한 전략을 쉽게 사용할 수 있도록 도와줌.
import { clientsClaim } from 'workbox-core';
// clientsClaim: 서비스 워커가 새로 설치된 후, 클라이언트(브라우저)에서 그 서비스를 바로 사용할 수 있도록 하는 역할
import { ExpirationPlugin } from 'workbox-expiration';
//캐시된 자원이 일정 시간 후에 만료되도록 설정하는 플러그인입니다. 이를 통해 오래된 자원을 자동으로 제거
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
//precacheAndRoute: 정적 자원(html, css,js,이미지) 등을 미리 캐시하여 빠르게 로딩할 수 있도록 함. createHandlerBoundToURL: 앱의 URL을 특정 index.html로 처리하는 라우터 핸들러를 생성
import { registerRoute } from 'workbox-routing';
//요청에 대한 라우팅 정의하여, 특정 URL에 대해 어떤 캐싱 전략을 사용할지 결정
import { StaleWhileRevalidate } from 'workbox-strategies';
//먼저 캐시된 데이터를 제공하고, 그 후에 서버에서 새로운 데이터를 가져오는 방식
declare const self: ServiceWorkerGlobalScope;
clientsClaim();
// 기본적으로 서비스 워커는 새로 설치되면 기존 서비스 워커가 종료될 때까지 기다린 후 활성화되는데, clientsClaim을 사용하면 기존의 서비스 워커를 기다리지 않고 새로운 워커가 바로 활성화되어 바로 서비스 시작할 수 있음.
// 새로운 서비스 워커가 설치되면 바로 활성화되어 클라이언트(웹페이지)에서 새로운 서비스를 사용할 수 있도록 하는 함수. 이전 서비스 워커가 종료될 때까지 기다리지 않고 즉시 새 워커가 클라언트를 제어하도록 함.
precacheAndRoute(self.__WB_MANIFEST);
//__WB_MNIFEST라는 특별한 변수를 사용하여 빌드 시 생성된 자원들을 미리 캐시함. 웹 앱의 주요 html, css,js,이미지 등을 미리 캐시해두고, 이후에는 그 캐시된 자원을 사용하여 빠르게 로딩
//앱의 Shell 스타일 라우팅 설정
// 이 코드는 싱글 페이지 애플리케이션에 사용됨.
// index.html 외에 다른 자원을 요청하는 경우에는 캐시하지 않도록 하는 정규 표현식
const fileExtensionRegexp = new RegExp('/[^/?]+\\\\.[^/]+$');
//registerRoute는 요청을 처리하는 규칙을 정의
// 사용자가 다른 페이지로 이동할 때마다 해당 페이지를 index.html로 처리하여 라우팅을 가능하게 함.
registerRoute(
({ request, url }: { request: Request; url: URL }) => {
if (request.mode !== 'navigate') { return false; }
if (url.pathname.startsWith('/_')) { return false; }
if (url.pathname.match(fileExtensionRegexp)) {
return false; }
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') // 파일 확장자를 가진 자원을 제외한 나머지 요청에 대해서는 index.html로 라우팅하게 설정
);
// 이미지 파일에 대한 캐시 전략 설정
// .png 확장자를 가진 이미지 파일을 처리
// -> 먼저 캐시된 데이터를 제공하고, 그 후에 서버에서 데이터를 가져와서 캐시를 갱신하는 전략 => 네트워크가 늦더라도 빠르게 로딩 가능, 최신 데이터는 백그라운드에서 가져옴.
registerRoute(
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// 캐시 이름을 images로 지정한 후, 캐시된 이미지가 50개를 초과하면 가장 오래된 이미지부터 자동으로 삭제됨.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// SKIP_WAITING 메시지 처리
// 서비스 워커가 새로 설치되면 이전 버전의 서비스 워커가 종료되지 않고 계속 실행 중일 수 있음 -> 이때 SKIP_WAITING 메시지를 받으면 self.skipWaiting 함수를 호출해서 기존의 서비스 워커를 종료하고, 새 워커로 바로 전환하도록 함
// => 업데이트가 즉시 적용될 수 있도록 도움.
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
전체적인 흐름 요약
- 서비스 워커 등록:
- 브라우저에서 웹 애플리케이션을 로드하면, 서비스 워커가 등록됩니다. 서비스 워커 파일은 브라우저에서 별도로 실행되며, 이 파일은 주로 service-worker.js라는 이름을 가집니다.
- 서비스 워커 설치:
- 서비스 워커가 처음 설치될 때, 앱의 중요한 파일들을 미리 캐시합니다. precacheAndRoute() 함수는 앱의 파일들을 미리 캐시하는 데 사용됩니다.
- 서비스 워커 활성화:
- 새로 설치된 서비스 워커가 기존 워커를 종료하고 활성화됩니다. clientsClaim()은 즉시 새로운 서비스 워커가 클라이언트를 제어하도록 합니다.
- 요청 처리:
- 사용자가 요청한 파일이나 리소스를 서비스 워커가 처리합니다. 예를 들어, 이미지 요청에 대해 StaleWhileRevalidate 전략을 사용하여 먼저 캐시된 이미지를 제공하고, 백그라운드에서 최신 이미지를 가져와서 캐시를 갱신합니다.
- 업데이트 처리:
- 서비스 워커가 새로 설치되면, SKIP_WAITING 메시지를 사용하여 바로 새 서비스 워커가 활성화되도록 할 수 있습니다.
서비스 워커 등록, 관리 코드
serviceWorkerRegistration.ts
서비스 워커의 등록과 업데이트 관리, 에러 처리를 담당
//현재 페이지가 로컬 서버에서 실행되고 있는지 확인하는 코드
//localhost, [::1], 127.x.x.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
window.location.hostname === '[::1]' ||
window.location.hostname.match(/^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
// register 함수
// 서비스 워커를 등록하는 주요 함수
// process.env.NODE_ENV === 'production' : 프로덕션 환경에서만 서비스 워커를 등록
// swUrl은 서비스 워커의 위치를 가리키며, 로컬 환경에서는 별도의 검사를 통해 서비스 워커가 유효한지 확인
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
const publicUrl = process.env.PUBLIC_URL ? new URL(process.env.PUBLIC_URL, window.location.href) : undefined;
if (publicUrl && publicUrl.origin !== window.location.origin) {
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
//로컬 서버에서 실행중이면 서비스 워커가 유효한지 검사
if (isLocalhost) {
checkValidServiceWorker(swUrl, config);
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit <https://cra.link/PWA>'
);
});
} else {
registerValidSW(swUrl, config);
}
});
}
}
//registerValidSW함수 :
// 프로덕션 환경에서는 서비스 워커 등록
//서비스 워커가 정상적으로 등록되었을 때 동작함
//서비스 워커가 업데이트될 때마다 onupdatefound 이벤트가 발생하며, 새로 설치된 워커가 준비되면 이를 알려줌.
function registerValidSW(swUrl: string, config?: Config) {
**navigator.serviceWorker
.register(swUrl) -- 서비스 워커 등록**
.then((registration) => {
//서비스 워커가 새로 설치되거나 업데이트될 때 발생
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
//서비스 워커가 설치되고 installed 상태가 되면, 새로운 콘텐츠가 준비되었음을 알리고, 캐시가 완료되었으면 onSuccess 콜백을 실행하거나, 업데이트가 필요하면 onUpdate 콜백을 실행
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
//controller가 존재한다면 이미 페이지가 활성화된 서비스 워커에 의해 제어되고 있다는 의미-> 새로운 콘텐츠 준비되었음.
console.log(
'새로운 콘텐츠 준비 완료 ' +
'이 페이지의 모든 탭을 닫으면 새로운 콘텐츠 적용됨. See <https://cra.link/PWA.'>
);
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
console.log('콘텐츠가 오프라인 사용을 위해 캐시됨');
//서비스 워커가 설치되고 오프라인용 콘텐츠가 캐시되었을 때 onSuccess 콜백이 호출됨. 이는 서비스 워커가 처음 활성화되었을 때 페이지가 오프라인 상태에서 실행될 수 있도록 캐시가 완료되었음을 의미
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error('에러 발생', error);
});
}
//checkValidServiceWorker함수: 서비스 워커 파일이 유효한지 확인하는 함수
//만약 404 에러나 javaScript 파일이 아닌 경우, 기존 서비스 워커를 삭제하고 페이지를 새로 고침
function checkValidServiceWorker(swUrl: string, config?: Config) {
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then((response) => {
const contentType = response.headers.get('content-type');
if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log('No internet connection found. App is running in offline mode.');
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister();
})
.catch((error) => {
console.error(error.message);
});
}
}
마지막으로 index.tsx 파일에 register 해주는 것 잊지 말기
- index.tsx는 리액트 애플리케이션의 진입점
- index.tsx에서 서비스 워커를 등록하는 이유 : 앱이 로드될 때 서비스 워커가 등록되도록 하기 위함 → 앱의 진입점에서 한 번만 등록하기 → 앱 전체에 걸쳐 적용
// index.tsx// ...
// 애플리케이션의 최상위 컴포넌트인 App을 렌더링함.
// ReactDOM.render()가 끝났다고 치고.. 앱이 렌더링된 상태에서 서비스 워커 등록하고 있는 상황 - 서비스 워커가 앱에 대한 모든 설정과 로딩 상태를 파악한 후에 등록중..
// 앱을 렌더링
root.render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<CookiesProvider>
<App />
</CookiesProvider>
</BrowserRouter>
</QueryClientProvider>
);
// 서비스 워커 등록
serviceWorkerRegistration.register();
✅ 전체적 요약
전체적으로, 이 파일은 Service Worker가 웹 앱의 자원들을 캐시하고, 새로운 데이터는 백그라운드에서 가져와서 캐시를 갱신하며, 사용자가 다른 페이지로 이동할 때 index.html을 이용해 라우팅을 처리하고, 이미지 캐싱을 위한 전략을 설정하는 역할을 한다. 또한, 서비스 워커가 새로 설치되면 즉시 활성화되어 사용자가 최신 앱을 사용할 수 있도록 도와준다.
✅ PWA를 적용한 기업
1. 스타벅스
PWA는 앱 내부에서 표시되는 언어는 달랐지만, 실제 GPS로 근처 매장을 선택해 메뉴를 주문하는 과정은 같음.
2. 트위터의 PWA 버전 “Twitter Lite”
이외에도 넷플릭스, 코스트코 등 많은 기업이 PWA 기술을 자사 서비스로 적용하여 사용자 경험과 만족도 높이는 결과를 얻을 수 있었다고 합니다.
'Developer Should Know' 카테고리의 다른 글
[Window] Windows 환경에서 8080 포트를 점유한 모든 프로세스를 한 번에 종료하는 명령어(kill) (0) | 2025.05.01 |
---|---|
github 작업하면서 생기는 질문 모음 .z..zip (계속 수정중) (0) | 2025.04.23 |
[article] 검색과 피드의 만남: LLM으로 완성하는 초개인화 서비스 (4) | 2025.04.02 |
[tech] 프론트엔드의 웹 접근성 (2) | 2025.03.30 |
과연 나는 현명한 개발자인가? (1) | 2025.02.26 |