FEInterview Prep

browser

브라우저 핵심 API — Fetch, Storage, Observer

프론트엔드 개발자는 매일 브라우저 API를 사용합니다. "Fetch와 XHR의 차이는?", "localStorage는 언제 쓰고 쿠키는 언제 쓰나요?", "IntersectionObserver를 어떻게 활용했나요?" 같은 질문이 실무형 면접에서 자주 나옵니다. 어떤 API를 언제, 왜 사용하는지 선택 기준을 설명할 수 있어야 합니다.

시작 전
이해도
매우 낮음

학습 개요

탄생 배경

해결하려 했던 문제

초기 웹은 서버 렌더링 중심으로 JavaScript의 역할이 제한적이었습니다. 동적 데이터 로딩(XHR), 클라이언트 상태 저장(쿠키), DOM 변화 감지 등이 모두 불편하거나 불가능했습니다.

역사적 맥락

XMLHttpRequest(2000년대 초)가 Ajax의 기반이 됐습니다. HTML5(2008-2014)에서 localStorage, sessionStorage, Canvas, Web Workers 등 강력한 API가 표준화됐습니다. Fetch API(2015)가 Promise 기반으로 XHR를 대체했고, IntersectionObserver(2016), ResizeObserver(2020) 등 Observer 패턴 API가 등장해 성능 친화적인 DOM 감지가 가능해졌습니다.

이전에는 어떻게 했나

Fetch API 대신 axios(더 간결한 API, 인터셉터), XHR(레거시 지원 필요 시), TanStack Query(서버 상태 관리). 스토리지 대신 IndexedDB(대용량 구조화 데이터), Cookie(서버 전송 필요 시). IntersectionObserver 대신 scroll 이벤트(레거시, 성능 나쁨).

멘탈 모델

동작 원리

Fetch API: - fetch(url, options)는 Promise<Response>를 반환 - Response.ok(200-299), Response.status, Response.json()으로 처리 - 4xx/5xx HTTP 에러는 자동으로 reject되지 않음 — res.ok 체크 필수 - AbortController로 요청 취소, credentials로 쿠키 포함 여부 제어 Web Storage: - localStorage: Origin 단위 영구 저장 (탭/창 공유, 명시적 삭제 전 유지) - sessionStorage: Origin + 탭 단위 임시 저장 (탭 닫으면 삭제) - 둘 다 5MB 제한, 동기 API, JS에서만 접근 - JSON.stringify/parse로 객체 저장 Observer APIs: - IntersectionObserver: 요소가 뷰포트에 보이는 비율 감지 (무한 스크롤, 지연 로딩) - ResizeObserver: 요소 크기 변화 감지 (레이아웃 반응형 처리) - MutationObserver: DOM 변화 감지 (서드파티 DOM 라이브러리 모니터링) - 모두 scroll/resize 이벤트보다 성능 우수 (메인 스레드 블로킹 없음)

핵심 구성 요소

Fetch API

Promise 기반 HTTP 요청. XHR 대체. AbortController로 취소 가능.

localStorage

영구적 클라이언트 저장소. 사용자 설정, 비로그인 장바구니 등.

sessionStorage

세션 단위 저장소. 다단계 폼 임시 데이터, 탭 간 격리 필요 시.

IntersectionObserver

요소의 뷰포트 진입/이탈 감지. 무한 스크롤, 이미지 지연 로딩.

ResizeObserver

요소 크기 변화 감지. window.resize보다 정확하고 효율적.

Canvas API

2D/WebGL 그래픽. 차트, 이미지 편집, 게임, 데이터 시각화.

흐름 설명


[Fetch API 올바른 에러 처리 패턴]

// ❌ 잘못된 코드 — 4xx/5xx도 resolve됨
try {
  const res = await fetch('/api/data');
  const data = await res.json();  // 404여도 실행됨
} catch (e) {
  // 네트워크 에러만 잡힘
}

// ✅ 올바른 코드
const res = await fetch('/api/data');
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
const data = await res.json();

[Web Storage 선택 기준]
로그인 유지 X, JS 접근 O, 영구 보관 → localStorage (테마, 언어 설정)
탭 간 격리 필요, 임시 데이터 → sessionStorage (다단계 폼)
서버 전송 필요, 보안 쿠키 → Cookie (인증 토큰 — HttpOnly)
대용량(5MB 이상) 구조화 → IndexedDB

[IntersectionObserver — 무한 스크롤]
const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadMoreItems();
  }
}, { threshold: 0.1 });

observer.observe(sentinelRef.current);
    

코드 예제


// ✅ Fetch + AbortController (타임아웃 구현)
async function fetchWithTimeout(url: string, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const res = await fetch(url, {
      signal: controller.signal,
      credentials: 'include',        // 쿠키 포함
      headers: { 'Content-Type': 'application/json' },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } finally {
    clearTimeout(timeoutId);
  }
}

// ✅ localStorage 타입 안전 유틸
function getStorageItem<T>(key: string, fallback: T): T {
  try {
    const item = localStorage.getItem(key);
    return item ? (JSON.parse(item) as T) : fallback;
  } catch {
    return fallback;
  }
}

// ✅ IntersectionObserver — React 무한 스크롤
function useInfiniteScroll(callback: () => void) {
  const sentinelRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) callback();
      },
      { threshold: 0.1 }
    );

    if (sentinelRef.current) observer.observe(sentinelRef.current);
    return () => observer.disconnect();
  }, [callback]);

  return sentinelRef;
}

// ✅ ResizeObserver — 요소 크기 감지
function useElementSize(ref: RefObject<HTMLElement>) {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const observer = new ResizeObserver(([entry]) => {
      setSize({
        width: entry.contentRect.width,
        height: entry.contentRect.height,
      });
    });
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, [ref]);

  return size;
}
    

비교 분석

브라우저

vs

Fetch API vs XHR (XMLHttpRequest)

비교 관점
이 방식
Fetch API vs XHR (XMLHttpRequest)
인터페이스
Fetch: Promise 기반 — async/await 친화적
XHR: 콜백 기반 — 콜백 헬 가능성
에러 처리
Fetch: HTTP 에러는 수동 체크 필요 (res.ok)
XHR: status 코드로 직접 분기
진행률
Fetch: ReadableStream으로 가능, 불편
XHR: onprogress 이벤트로 편리
Service Worker
Fetch: 완전 지원 (fetch 이벤트 인터셉트)
XHR: Service Worker에서 사용 불가
취소
Fetch: AbortController
XHR: xhr.abort()

브라우저

vs

localStorage vs Cookie

비교 관점
이 방식
localStorage vs Cookie
용량
localStorage: ~5MB
Cookie: ~4KB
서버 전송
localStorage: 안 됨 — JS에서만
Cookie: 매 요청마다 자동 전송
보안 옵션
localStorage: 없음 — XSS에 취약
Cookie: HttpOnly, Secure, SameSite 설정 가능
만료 설정
localStorage: 없음 (명시 삭제 전 영구)
Cookie: expires/max-age로 만료 설정

브라우저

vs

IntersectionObserver vs scroll 이벤트

비교 관점
이 방식
IntersectionObserver vs scroll 이벤트
성능
IntersectionObserver: 비동기, 메인 스레드 블로킹 없음
scroll: 동기, 잦은 호출로 레이아웃 스래싱 위험
정확도
IntersectionObserver: 뷰포트 교차 비율(threshold) 제공
scroll: getBoundingClientRect() 반복 호출 필요
구현 복잡도
약간 복잡하지만 패턴화 가능
직관적이지만 throttle/debounce 필수

트레이드오프

이상적인 사용 사례

면접 질문