본문 바로가기

server/system design

인터넷 연결이 안되었는데 youtube 페이지는 어떻게 나오는걸까?

 

1. 인터넷을 연결해주세요

나는 항상 모뎀을 끄고 다닌다. 굳이 내가 없는 집에 전기세가 아까우니까.

집에 와서 컴퓨터를 키면 몇 초 정도 인터넷이 되지 않아, 유투브에 들어가면 위와 같은 화면이 나를 반긴다.

 

근데...인터넷이 연결되지 않았는데 어떻게 화면이 그려지지???

 

답은 캐싱이다. 더 정확히는 서비스 워커를 이용한 캐싱이다.

 

서비스 워커는 웹사이트의 리소스(HTML, CSS, JavaScript, 이미지 등)를 브라우저의 캐시에 저장하고, 오프라인 상태에서도 이 캐시된 리소스를 사용할 수 있게 한다.

유투브 이름으로 된 저장소를 보면 offline상태에서 실행되는 js를 확인할수 있다.

 

 

2. 서비스 워커(Service Worker)

서비스 워커는 웹 애플리케이션의 성능을 향상시키고, 오프라인 기능을 제공하는 강력한 도구이다. 브라우저의 백그라운드에서 실행되는 스크립트로, 네트워크 요청을 가로채고 캐시를 관리하며, 오프라인 상태에서도 웹사이트가 작동할 수 있도록 한다.

 

서비스 워커는 보통 다음 사례에서 사용된다.

 

오프라인 기능 제공 : 인터넷 연결이 없는 상태에서도 웹사이트가 동작하도록 한다.

성능 최적화: 자주 사용되는 리소스를 캐시에 저장해 로딩 속도 가속화 (이미지 / css / js 등)

백그라운드 작업: 사용자가 웹사이트를 떠난 후에도 백그라운드에서 데이터를 동기화하거나 작업을 수행

 

서비스 워커는 오프라인 지원을 통해 인터넷 연결 없이도 웹사이트가 동작하도록 하고, 캐시를 통해 리소스 로딩을 향상 시키기때문에 자주 많이 사용된다.

 

단점으로는 캐시를 계속 해서 관리해야 한다. 리소스가 업데이트 되면 계속해서 캐시를 갱신하지 않으면 오래된 데이터가 표기된다.

브라우저 호환성과 특히 HTTPS에서만 동작하도록 브라우저에서 통제한다.  마지막으로 메모리와 배터리가 백그라운드에서 사용되는것을 감안해야 한다.

 

 

 


3. 구현

서비스워커를 구현해서 유투브처럼 오프라인에서도 홈페이지가 나올수 있도록 만들어보자.

index.html / scripts.js / service-worker.js / style.css 는 public 폴더안에 저장하자 (아래에 나올 Node.js 서버에서 public을 바라보도록 구성했다)

 

index.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>유튜브 클론</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <header>
      <div class="logo">
        <img
          src="https://www.youtube.com/yt/brand/media/image/YouTube-logo-full_color.png"
          alt="YouTube Logo"
        />
      </div>
      <div class="search-bar">
        <input type="text" placeholder="검색" />
        <button>검색</button>
      </div>
      <div class="menu">
        <button>+ 만들기</button>
        <button>알림</button>
        <button>프로필</button>
      </div>
    </header>
    <main>
      <!-- 인터넷 연결 상태에 따라 동적으로 표시 -->
      <div class="content" id="content">
        <!-- 기본적으로 오프라인 메시지를 표시 -->
        <div class="offline-message">
          <img src="https://via.placeholder.com/150" alt="Offline Icon" />
          <h1>오프라인 상태입니다.</h1>
          <p>오류가 발생했습니다. 여기 상태를 확인하세요.</p>
          <button id="retry-button">다시 시도</button>
        </div>
        <!-- 온라인 상태일 때 표시할 콘텐츠 -->
        <div class="online-content" style="display: none">
          <h1>온라인 상태입니다!</h1>
          <p>인터넷이 연결되어 있습니다. 콘텐츠를 즐기세요!</p>
        </div>
      </div>
    </main>
    <nav>
      <ul>
        <li><a href="#">홈</a></li>
        <li><a href="#">Shorts</a></li>
        <li><a href="#">구독</a></li>
        <li><a href="#">더 탐색하기</a></li>
      </ul>
    </nav>
    <script src="scripts.js"></script>
    <script src="service-worker.js"></script>
    <!-- 서비스 워커 파일 연결 -->
  </body>
</html>

 

 

style.css

body {
  margin: 0;
  font-family: Arial, sans-serif;
  background-color: #121212;
  color: #ffffff;
}

header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 20px;
  background-color: #202020;
}

.logo img {
  width: 150px;
}

.search-bar {
  display: flex;
  align-items: center;
  margin-left: 20px;
}

.search-bar input {
  width: 400px;
  padding: 5px;
  border: 1px solid #cccccc;
  border-radius: 2px;
}

.search-bar button {
  padding: 5px 10px;
  margin-left: 10px;
  background-color: #ffffff;
  color: #000000;
  border: none;
  border-radius: 2px;
  cursor: pointer;
}

.menu {
  display: flex;
  gap: 10px;
}

.menu button {
  padding: 5px 10px;
  background-color: transparent;
  color: #ffffff;
  border: 1px solid #ffffff;
  border-radius: 2px;
  cursor: pointer;
}

main {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80vh;
  text-align: center;
}

.offline-message img {
  width: 100px;
  margin-bottom: 20px;
}

.offline-message h1 {
  font-size: 24px;
  margin-bottom: 10px;
}

.offline-message p {
  font-size: 16px;
  margin-bottom: 20px;
}

.offline-message button {
  padding: 10px 20px;
  background-color: #03a9f4;
  color: #ffffff;
  border: none;
  border-radius: 2px;
  cursor: pointer;
}

.online-content {
  text-align: center;
}

nav {
  background-color: #202020;
  padding: 10px 20px;
}

nav ul {
  list-style: none;
  padding: 0;
  display: flex;
  gap: 20px;
}

nav ul li a {
  color: #ffffff;
  text-decoration: none;
  font-size: 16px;
}

 

 

scripts.js

 

  • navigator.onLine을 사용해 현재 네트워크 상태를 확인합니다. (브라우저에서 제공하는 네트워크 동작 확인 함수)
  • online과 offline 이벤트를 감지해 네트워크 상태가 변경될 때마다 UI를 업데이트

 

document.addEventListener("DOMContentLoaded", () => {
  const content = document.getElementById("content");
  const offlineMessage = document.querySelector(".offline-message");
  const onlineContent = document.querySelector(".online-content");
  const retryButton = document.getElementById("retry-button");

  // 초기 상태 설정
  updateNetworkStatus();

  // 네트워크 상태 변경 이벤트 리스너
  window.addEventListener("online", updateNetworkStatus);
  window.addEventListener("offline", updateNetworkStatus);

  // 다시 시도 버튼 클릭 시 네트워크 상태 확인
  retryButton.addEventListener("click", () => {
    updateNetworkStatus();
  });

  function updateNetworkStatus() {
    if (navigator.onLine) {
      // 온라인 상태
      offlineMessage.style.display = "none";
      onlineContent.style.display = "block";
    } else {
      // 오프라인 상태
      offlineMessage.style.display = "block";
      onlineContent.style.display = "none";
    }
  }

  // 서비스 워커 등록
  if ("serviceWorker" in navigator) {
    window.addEventListener("load", () => {
      navigator.serviceWorker
        .register("/service-worker.js")
        .then((registration) => {
          console.log("서비스 워커 등록 성공:", registration);
        })
        .catch((error) => {
          console.error("서비스 워커 등록 실패:", error);
        });
    });
  }
});

 

 

service-worker.js

- 브라우저 Cache Storage 섹션에서 my-cache-v1이라는 이름의 캐시가 생성되도록 코드 작성.

- 서비스 워커는 HTTPS 환경에서만 동작합니다. 로컬 개발 환경에서는 localhost를 사용해야 한다.

const CACHE_NAME = "my-cache-v1";
const urlsToCache = ["/", "/style.css", "/scripts.js"];

// 서비스 워커 설치
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log("캐시에 리소스를 저장 중...");
      return cache.addAll(urlsToCache);
    })
  );
});

// 캐시된 리소스 가져오기
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // 캐시에서 리소스를 찾을 수 있으면 반환
      if (response) {
        return response;
      }
      // 캐시에 없으면 네트워크에서 가져옴
      return fetch(event.request);
    })
  );
});

 

 

간단한 node.js 서빙 코드

const express = require("express");
const path = require("path");

// Express 앱 생성
const app = express();
const PORT = 3000;

// 정적 파일 제공 (HTML, CSS, JS, 이미지 등)
app.use(express.static(path.join(__dirname, "public")));

// 루트 경로에 대한 요청 처리
app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

// 서버 시작
app.listen(PORT, () => {
  console.log(`서버가 http://localhost:${PORT}에서 실행 중입니다.`);
});

 

이제 서버를 실행하고,

node server.js

 

브라우저를 열고 http://localhost:3000에 접속합니다. index.html 파일이 로드되고, 다음과 같은 화면이 나온다. (인터넷에 연결되었을때)

 

그리고 브라우저의 캐시에도 작성한 파일들이 저장된것을 볼수 있다. (서비스 워커 동작 확인)

 


혹시나 로컬 서버가 아닌 그냥 index.html 파일을 열어서 확인한다면 다음과 같은 에러를 볼수 있다.

서비스 워커는 기본적으로 https 프로토콜을 사용해야만 동작한다. (로컬에서는 localhost만 동작한다)

서비스 워커 등록 실패: TypeError: ServiceWorkerContainer.register: Script URL's scheme is not 'http' or 'https'
    <anonymous> file:///Users/uiandwe/develop/chat-app/scripts.js:35
    EventListener.handleEvent* file:///Users/uiandwe/develop/chat-app/scripts.js:33
    EventListener.handleEvent* file:///Users/uiandwe/develop/chat-app/scripts.js:1
scripts.js:40:19

 

이제 인터넷을 끄고 다시 화면을 확인해보면 다음과 같이 오프라인 상태로 표기되는것을 확인할수 있다.

물론 node 서버를 꺼도 같은 화면이 나온다. (캐시 저장소의 파일들을 이용해서 화면을 보여주기 때문이다.)