브라우저에는 이미 엄청나게 많은 기능이 내장되어 있다. 스크롤 감지, 백그라운드 연산, 오프라인 지원 — 이걸 전부 외부 라이브러리로 해결하려고 했던 적이 있는가?


브라우저 내장 API의 가치

jQuery 시절에는 스크롤 위치를 계산하려면 scroll 이벤트에 getBoundingClientRect()를 매 프레임 호출했다. 무거운 계산이 메인 스레드를 점유하면 UI가 버벅였고, 오프라인은 아예 생각도 못 했다.

지금은 다르다. 브라우저가 직접 제공하는 Web API만으로도 충분히 많은 걸 할 수 있다.

  • Intersection Observer — 스크롤 기반 요소 감지를 브라우저에 위임
  • Web Worker — 무거운 연산을 별도 스레드에서 실행
  • Service Worker — 네트워크 요청을 가로채서 오프라인 지원, 캐시 전략 적용
  • Geolocation API — 사용자 위치 정보 획득
  • Notification API / Push API — 사용자에게 알림 전송
  • Performance Observer — Core Web Vitals 같은 성능 지표 수집

라이브러리를 추가하기 전에, 브라우저가 이미 제공하는 걸 먼저 확인하는 습관이 중요하다.


Intersection Observer

왜 scroll 이벤트 대신 쓰는가

scroll 이벤트로 요소의 가시성을 판단하려면 매 스크롤마다 getBoundingClientRect()를 호출해야 한다. 이 메서드는 ** 강제 리플로우(forced reflow)**를 발생시킨다. 요소가 10개, 20개 늘어나면 성능이 눈에 띄게 떨어진다.

Intersection Observer는 이 판단을 ** 브라우저 엔진에 위임 **한다. 브라우저가 최적화된 방식으로 교차 여부를 감지하고, 조건이 충족될 때만 콜백을 호출한다.

기본 사용법

JAVASCRIPT
// 옵저버 생성
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      // 요소가 뷰포트에 진입했을 때
      console.log('보인다:', entry.target);
    }
  });
}, {
  root: null,          // 기본값: 뷰포트
  rootMargin: '0px',   // 뷰포트 마진 (CSS margin 형식)
  threshold: 0         // 0~1 사이, 또는 배열
});

// 관찰 대상 등록
observer.observe(document.querySelector('.target'));

threshold와 rootMargin

이 두 옵션이 Intersection Observer의 핵심이다.

  • threshold — 타겟 요소가 얼마나 보여야 콜백을 호출할지

    • 0: 1px이라도 보이면 호출 (기본값)
    • 0.5: 절반이 보이면 호출
    • 1: 전체가 보여야 호출
    • [0, 0.25, 0.5, 0.75, 1]: 각 비율마다 호출
  • rootMargin — 감지 영역을 확장하거나 축소

    • '100px 0px': 뷰포트 위아래로 100px 확장 → 요소가 보이기 ** 전에** 미리 감지
JAVASCRIPT
// 뷰포트 진입 200px 전에 미리 감지 — 이미지 프리로딩에 유용
const observer = new IntersectionObserver(callback, {
  rootMargin: '200px 0px',
  threshold: 0
});

실전: 이미지 Lazy Loading

HTML
<img data-src="heavy-image.jpg" class="lazy" alt="설명">
JAVASCRIPT
const lazyObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;  // 실제 이미지 로드
      img.classList.remove('lazy');
      observer.unobserve(img);    // 로드 후 관찰 해제
    }
  });
}, {
  rootMargin: '100px 0px'  // 100px 전에 미리 로드 시작
});

// 모든 lazy 이미지 등록
document.querySelectorAll('img.lazy').forEach((img) => {
  lazyObserver.observe(img);
});

참고로 요즘 브라우저는 <img loading="lazy">를 네이티브로 지원한다. 단순 이미지 lazy loading이라면 이쪽이 더 간단하다. Intersection Observer는 애니메이션 트리거, 무한 스크롤 같은 ** 커스텀 로직 **이 필요할 때 빛난다.

실전: 무한 스크롤

JAVASCRIPT
// 리스트 마지막에 센티널(감시용) 요소 배치
const sentinel = document.querySelector('.scroll-sentinel');

const scrollObserver = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadMoreItems();  // 다음 페이지 데이터 로드
  }
});

scrollObserver.observe(sentinel);

기존 scroll 이벤트 + 스크롤 위치 계산 방식보다 훨씬 깔끔하고, 성능 부담도 적다.


Web Workers

메인 스레드를 막으면 안 되는 이유

자바스크립트는 ** 싱글 스레드 **다. UI 렌더링, 이벤트 처리, 자바스크립트 실행이 전부 하나의 스레드에서 돌아간다. 여기서 무거운 연산을 돌리면?

  • 버튼 클릭이 안 먹는다
  • 애니메이션이 끊긴다
  • 사용자가 "이 사이트 멈췄나?" 하고 떠난다

Web Worker는 ** 별도 스레드에서 자바스크립트를 실행 **할 수 있게 해준다.

기본 사용법

JAVASCRIPT
// worker.js — 별도 파일
self.addEventListener('message', (e) => {
  const { numbers } = e.data;

  // 무거운 연산 (예: 대량 데이터 정렬)
  const sorted = numbers.sort((a, b) => a - b);

  // 결과를 메인 스레드로 전달
  self.postMessage({ sorted });
});
JAVASCRIPT
// main.js — 메인 스레드
const worker = new Worker('worker.js');

// 워커에 데이터 전달
worker.postMessage({
  numbers: [5, 3, 8, 1, 9, 2, 7, 4, 6]
});

// 워커로부터 결과 수신
worker.addEventListener('message', (e) => {
  console.log('정렬 완료:', e.data.sorted);
});

// 에러 처리
worker.addEventListener('error', (e) => {
  console.error('워커 에러:', e.message);
});

// 더 이상 필요 없으면 종료
// worker.terminate();

Web Worker의 제약

Worker는 메인 스레드와 완전히 분리된 환경에서 돌아간다. 그래서:

  • DOM 접근 불가document, window 사용 불가
  • ** 통신은 postMessage만** — 데이터는 구조화된 복제(structured clone)로 전달
  • ** 별도 파일 필요** — 인라인으로 만들려면 Blob URL 사용
JAVASCRIPT
// Blob URL로 인라인 워커 생성 (별도 파일 없이)
const workerCode = `
  self.onmessage = (e) => {
    const result = e.data * 2;
    self.postMessage(result);
  };
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));

SharedWorker

일반 Worker는 생성한 페이지에서만 사용할 수 있다. SharedWorker 는 같은 origin의 여러 탭/윈도우에서 공유할 수 있다.

JAVASCRIPT
// shared-worker.js
const connections = [];

self.addEventListener('connect', (e) => {
  const port = e.ports[0];
  connections.push(port);

  port.addEventListener('message', (event) => {
    // 모든 연결된 탭에 메시지 브로드캐스트
    connections.forEach((conn) => {
      conn.postMessage(`브로드캐스트: ${event.data}`);
    });
  });

  port.start();
});
JAVASCRIPT
// 각 탭에서
const shared = new SharedWorker('shared-worker.js');
shared.port.start();
shared.port.postMessage('안녕하세요');
shared.port.onmessage = (e) => console.log(e.data);

탭 간 실시간 상태 동기화가 필요할 때 SharedWorker가 유용하다. 다만 브라우저 지원 범위를 꼭 확인하자.


Service Worker

웹 앱을 오프라인에서도 돌릴 수 있다고?

Service Worker는 브라우저와 네트워크 사이에 위치하는 프록시 다. 네트워크 요청을 가로채서 캐시된 응답을 반환하거나, 요청을 수정하거나, 완전히 새로운 응답을 만들어낼 수 있다.

PWA(Progressive Web App)의 핵심 기술이기도 하다.

생명주기

Service Worker는 일반 자바스크립트와 다른 독립적인 생명주기 를 가진다.

  1. 등록(Register) — 메인 스크립트에서 navigator.serviceWorker.register() 호출
  2. ** 설치(Install)** — 캐시할 리소스를 미리 저장
  3. ** 활성화(Activate)** — 이전 버전의 캐시 정리
  4. Fetch 가로채기 — 네트워크 요청을 인터셉트
JAVASCRIPT
// main.js — 등록
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then((reg) => console.log('SW 등록 성공:', reg.scope))
    .catch((err) => console.error('SW 등록 실패:', err));
}
JAVASCRIPT
// sw.js — 설치 단계에서 캐시 준비
const CACHE_NAME = 'v1';
const PRECACHE_URLS = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/offline.html'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(PRECACHE_URLS))
  );
});

캐시 전략

여기가 Service Worker의 진짜 가치다. 상황에 따라 다른 캐시 전략을 적용할 수 있다.

Cache First — 캐시 우선

정적 자산(CSS, JS, 이미지)에 적합하다. 캐시에 있으면 바로 반환하고, 없을 때만 네트워크로 간다.

JAVASCRIPT
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((cached) => cached || fetch(event.request))
  );
});

Network First — 네트워크 우선

API 응답처럼 ** 최신 데이터가 중요한** 경우. 네트워크를 먼저 시도하고, 실패하면 캐시에서 반환한다.

JAVASCRIPT
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // 응답을 캐시에도 저장
        const clone = response.clone();
        caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
        return response;
      })
      .catch(() => caches.match(event.request))
  );
});

Stale-While-Revalidate — 캐시 반환 + 백그라운드 갱신

** 속도와 최신성을 동시에** 확보하는 전략이다. 캐시된 응답을 즉시 반환하면서, 백그라운드에서 네트워크 응답으로 캐시를 갱신한다.

JAVASCRIPT
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then((cache) =>
      cache.match(event.request).then((cached) => {
        // 백그라운드에서 캐시 갱신
        const fetchPromise = fetch(event.request).then((response) => {
          cache.put(event.request, response.clone());
          return response;
        });
        // 캐시가 있으면 즉시 반환, 없으면 네트워크 대기
        return cached || fetchPromise;
      })
    )
  );
});

활성화 — 이전 캐시 정리

새 버전을 배포할 때, 이전 버전의 캐시를 정리하는 것도 중요하다.

JAVASCRIPT
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys
          .filter((key) => key !== CACHE_NAME)  // 현재 버전이 아닌 캐시 삭제
          .map((key) => caches.delete(key))
      )
    )
  );
});

Service Worker는 HTTPS 환경에서만 동작한다 (localhost 제외). 보안이 보장되지 않는 환경에서 네트워크 요청을 가로채는 건 위험하니까.


Geolocation API

사용자 위치 가져오기

GPS, Wi-Fi, IP 주소 등을 조합해서 사용자의 현재 위치를 알아낸다. 당연히 ** 사용자 동의(권한)**가 필요하다.

JAVASCRIPT
// 한 번 위치 가져오기
navigator.geolocation.getCurrentPosition(
  (position) => {
    console.log('위도:', position.coords.latitude);
    console.log('경도:', position.coords.longitude);
    console.log('정확도:', position.coords.accuracy, '미터');
  },
  (error) => {
    // 에러 처리
    switch (error.code) {
      case error.PERMISSION_DENIED:
        console.log('사용자가 위치 접근을 거부했습니다');
        break;
      case error.POSITION_UNAVAILABLE:
        console.log('위치 정보를 사용할 수 없습니다');
        break;
      case error.TIMEOUT:
        console.log('위치 요청이 시간 초과되었습니다');
        break;
    }
  },
  {
    enableHighAccuracy: true,  // GPS 우선 (배터리 소모 증가)
    timeout: 10000,            // 10초 제한
    maximumAge: 0              // 캐시된 위치 사용 안 함
  }
);

watchPosition — 실시간 위치 추적

JAVASCRIPT
// 위치가 변할 때마다 콜백 호출
const watchId = navigator.geolocation.watchPosition(
  (position) => {
    updateMap(position.coords.latitude, position.coords.longitude);
  },
  handleError,
  { enableHighAccuracy: true }
);

// 추적 중단
navigator.geolocation.clearWatch(watchId);

배달 앱의 라이더 위치 추적, 러닝 앱의 경로 기록 같은 기능이 이 API 하나로 가능하다.


Web Storage vs IndexedDB

이 주제는 자바스크립트 영역이라 여기서는 간략하게만 비교하고 넘어간다.

구분localStoragesessionStorageIndexedDB
용량~5MB~5MB수백 MB 이상
유지영구 (직접 삭제 전까지)탭 닫으면 삭제영구
데이터 형식문자열만문자열만객체, Blob 등
동기/비동기동기동기비동기
용도간단한 설정값임시 상태대량 구조화 데이터
JAVASCRIPT
// localStorage 기본 사용
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme');  // 'dark'
localStorage.removeItem('theme');

// 객체를 저장하려면 직렬화 필요
localStorage.setItem('user', JSON.stringify({ name: '홍길동' }));
const user = JSON.parse(localStorage.getItem('user'));

IndexedDB의 비동기 API와 트랜잭션 모델은 별도 자바스크립트 글에서 자세히 다룰 예정이다.


Notification API와 Push API

Notification API — 브라우저 알림

사용자에게 시스템 알림을 보낼 수 있다. 역시 ** 권한 요청이 필수 **다.

JAVASCRIPT
// 1. 권한 요청
Notification.requestPermission().then((permission) => {
  if (permission === 'granted') {
    // 2. 알림 생성
    const notification = new Notification('새 메시지', {
      body: '홍길동님이 메시지를 보냈습니다.',
      icon: '/icons/message.png',
      tag: 'message-1'  // 같은 tag의 알림은 하나만 표시
    });

    // 3. 알림 클릭 이벤트
    notification.onclick = () => {
      window.focus();
      notification.close();
    };
  }
});

Push API — 서버에서 푸시

Notification API가 "클라이언트에서 직접 알림을 띄우는 것"이라면, Push API는 ** 서버가 보낸 메시지를 Service Worker가 받아서 알림을 띄우는 것 **이다.

JAVASCRIPT
// Service Worker에서 push 이벤트 수신
self.addEventListener('push', (event) => {
  const data = event.data.json();

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: data.icon,
      data: { url: data.url }  // 클릭 시 이동할 URL
    })
  );
});

// 알림 클릭 처리
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  event.waitUntil(
    clients.openWindow(event.notification.data.url)
  );
});

Push API를 쓰려면 서버 측에 Web Push 프로토콜 구현이 필요하다. VAPID 키 쌍을 생성하고, 구독 정보를 서버에 저장하는 과정이 수반된다. 이건 백엔드 주제에 가깝다.


Performance Observer

성능을 "감"이 아니라 "숫자"로 측정하기

Core Web Vitals(LCP, FID, CLS) 같은 성능 지표를 자바스크립트로 직접 수집할 수 있다. Google Analytics나 별도 모니터링 서비스로 보내는 RUM(Real User Monitoring) 데이터의 기반이 되는 API다.

JAVASCRIPT
// LCP (Largest Contentful Paint) 측정
const lcpObserver = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP:', lastEntry.startTime, 'ms');
});

lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
JAVASCRIPT
// Long Task 감지 — 50ms 이상 걸리는 작업
const longTaskObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.warn('Long Task 감지:', entry.duration, 'ms');
    // 모니터링 서비스로 전송
  });
});

longTaskObserver.observe({ type: 'longtask', buffered: true });
JAVASCRIPT
// 리소스 로딩 시간 측정
const resourceObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    if (entry.duration > 1000) {
      console.warn('느린 리소스:', entry.name, entry.duration, 'ms');
    }
  });
});

resourceObserver.observe({ type: 'resource', buffered: true });

PerformanceEntry 타입 정리

타입설명
navigation페이지 로딩 전체 타이밍
resource개별 리소스(이미지, CSS, JS) 로딩 시간
largest-contentful-paintLCP — 가장 큰 콘텐츠가 렌더링된 시점
first-inputFID — 첫 사용자 입력에 대한 응답 지연
layout-shiftCLS — 예기치 않은 레이아웃 이동
longtask50ms 이상 메인 스레드를 점유한 작업
paintFirst Paint, First Contentful Paint

실무에서는 web-vitals 라이브러리를 많이 쓰지만, 내부적으로 PerformanceObserver를 사용한다는 걸 알아두면 디버깅할 때 도움이 된다.


정리

API핵심 역할대표 사용처
Intersection Observer요소 가시성 감지Lazy loading, 무한 스크롤, 애니메이션
Web Worker별도 스레드 연산대량 데이터 처리, 이미지 변환
Service Worker네트워크 프록시오프라인 지원, PWA, 캐시 전략
Geolocation위치 정보지도, 배달 추적, 날씨
Notification / Push사용자 알림채팅 알림, 뉴스 알림
Performance Observer성능 지표 수집Core Web Vitals, RUM

외부 라이브러리를 설치하기 전에, 브라우저가 이미 제공하는 API부터 확인하자. 번들 크기도 줄이고, 브라우저 최적화의 혜택도 받을 수 있다.

댓글 로딩 중...