(번역) 웹 개발에서 img 태그 다루는 법

이 글은 Don’t ruin your 라는 미디엄 글을 번역한 것입니다.

서론

  • 개발자와 디자이너에게 있어서 최고의 사용자 경험을 제공하는 것은 가장 중요한 일입니다.
  • 더 나은 사용자 경험을 위해서 중요한 한 부분을 차지하는 것은 Image 인데요.
  • 대부분의 개발자들은 이런 이미지에 대해서 src 속성을 추가하는 것과 크기 조절하는 것 외에는 크게 신경쓰지 않습니다.
  • 이번 가이드에서는 이미지 사용에 있어 흔한 문제들을 살펴보고, 해결하는 방법에 대해서 알아보겠습니다.
  • 참고 : 이미지 사용에 있어서 대표적인 문제점들을 여기에 올려 두었습니다. 같은 이미지가 사용되었으니, 다른 유형들을 확인할 때 강력 새로고침으로 캐쉬를 비워주세요.

이미지 용량 줄이기 (Cut images down to size)

  • 웹 동작방식에 대해서 간단히 얘기 드리면,
    1. 웹 브라우저에 접속하면 HTML 페이지가 웹 브라우저로 전송됩니다.
    2. 브라우저는 이 HTML 페이지가 갖고 있는 모든 자원들(Javascript, stylesheet, images)을 다운받아야 합니다.
    3. 각 자원들을 로드하기 위해서는 시간이 소요되고, 파일이 클수록 이 시간이 길어집니다.
  • 우리는 종종 Javascript 파일을 압축하여 더 작은 파일로 만드는데요. Image 도 동일하게 해줘야 합니다.
  • 여기서 이미지의 쓸데없는 용량 낭비를 막는 방법 중 한 가지는 이미지의 픽셀 크기를 조절하는 것입니다. 이미지가 쓸데 없이 클 필요는 없으니까요.
  • 예를 들어, 당신이 사용하려는 웹사이트의 이미지가 960px 인데, 이미지 파일의 실제 너비는 1800px 이면 로딩시간이 필요이상으로 지체가 됩니다. 특히나, 느린 인터넷을 사용할 때는 이 고통이 더 극심해지죠.
  • 이미지의 실제 치수를 알 때에는 SketchAffinity Designer 를 이용하여 이미지 크기를 재 조정 할 수 있습니다.
  • 더 가시적인 사례로 예를 들어보겠습니다.

image_1
– 위 그림에서, 브라우저에서 사용할 이미지의 크기는 960px 인데, 여기서 사용되는 이미지 파일의 실제 너비는 5183px 입니다. 적절한 크기의 이미지를 사용하게 되면, 크기가 90% 감소 / 로딩시간은 6초 향상 됩니다. 이 도표는 사실 인터넷의 속도에 따라 천차만별이 됩니다. 느릴 인터넷 일수록 더 차이가 많이 날 것입니다.

이미지 압축 하기 (Use compression)

  • 이미지 치수를 줄여서 파일 크기를 줄일 수도 있지만, 압축을 이용해서 더 줄일 수 있습니다.
  • 이미지 압축은 이미지 파일의 크기를 줄이고, 이미지의 품질을 살짝 저하시킵니다.
  • 이미지 압축은 metadata, embedded thumbnails, color profiles 등의 불필요한 값들을 제거해줍니다.
  • 맥 사용자의 경우 ImageOptim 프로그램으로 이미지 압축을 할 수 있고, GruntGulp 같은 task runner 를 활용하여 프로젝트 빌드시에 이미지 압축을 자동화 할 수도 있습니다.
  • 아래 그림은 압축한 이미지로 크기를 13% 줄이는 모습입니다.

image_3
– 이 것도 역시 Wifi가 아닌 3G 와 같은 느린 인터넷에서는 더 많이 차이가 나겠죠?

미디어쿼리로 반응형 이미지 적용 (Use media queries to make images responsive)

  • 앞 두 단계에서 이미지 파일을 줄였지만, 나은 사용자 경험을 위해서 저희가 더 해야 할 일은 이미지를 반응형으로 만드는 것입니다.
  • 종종 반응형 이미지를 제공하지 않아, 사용자가 일일이 브라우저 스크롤을 왼쪽 오른쪽으로 움직여 이미지를 확인해야 하는 사이트가 있습니다. 이런 사이트는 사용자 비친화적인 사이트로 분류됩니다.
  • 미디어쿼리를 사용하여 브라우저가 특정 크기가 되면, max-width: 100% 와 같이 이미지 치수를 조절하세요.
  • 예제 를 보시면 반응형 이미지가 항상 브라우저의 크기에 맞춰지기 때문에 사용자가 이미지 확대나 축소를 할 필요가 없어집니다.

예술 감독한테 물어보세요 (Ask an art director)

  • 앞 단계들에서 축소한 이미지들은 대개 저희가 원하는 품질을 가지고 있지 않습니다.
  • 위 예제 에서 사용된 사진 속 뉴욕 엠파이어 스테이트 빌딩은 브라우저의 크기가 작아졌을 때, 거의 알아보기가 힘듭니다.
  • 이럴 때 picture 나 source 같은 HTML5 태그를 이용하여 이미지의 부분을 자르거나 변경합니다. 이를 예술 감독 이라고 합니다.
  • 위 태그는 사용된 미디어쿼리에 따라 브라우저에게 어떤 이미지를 요청하고 사용할 지를 알려줍니다.
  • 예제 를 확인해보시면, 브라우저 사이즈에 따라 다른 이미지를 요청하게 되는 걸 볼 수 있습니다.

서비스워커로 캐싱하기 (Use service workers to cash in on caching)

  • 반응형 디자인은 상대적으로 새로운 컨셉입니다. 어떤 디스플레이를 사용하든 웹사이트를 이쁘게 보이게 해야하죠.
  • 그런데, 요즘은 웹 사이트를 마치 Native Apps 처럼 느끼게 해주는 새로운 트렌드가 나왔습니다.
  • 이 트렌드를 Progressive Web Apps 이라고 하고, 인터넷 연결 없이도 웹사이트가 돌아가게 해주는 Service Worker 를 포함하고 있죠.
  • 이러한 PWA 가 이미지에게 의미하는 바는 뭘까요? 사이트 접속 시 자주 업데이트 되지 않는 로고나 기타 정적인 이미지는 캐쉬 되는 것이 성능 관점에서 효율적입니다. 캐쉬가 된 파일들은 Client 브라우저에 저장됩니다.
  • 캐쉬를 하게 되면, 이후에 사용자가 웹 사이트에 접속할 때 HTTP 요청을 날리기 전에 먼저 캐쉬를 확인합니다.
  • 보통 캐쉬를 활용하는 것이 HTTP 요청보다 더 빠르기 때문에, 로딩시간이 더 빨라집니다.
  • 그리고, 이미지를 캐쉬하였기 때문에 인터넷 연결 없이도 이미지를 사용할 수 있습니다.
  • Service Worker는 브라우저와 인터넷 사이의 Middleware 역할을 하는 강력한 기술 입니다.

접근성이 높은 이미지 만들기 (An accessible image is a friendly image)

  • <img /> 태그를 사용할 떄, alt 속성을 꼭 사용합니다.
  • 눈이 안보이는 시각 장애인 분들의 경우, 웹 페이지의 내용을 확인할 때. 스크린 리더 라는 도구를 활용합니다.
  • 이 도구는 이미지를 스캔할 때, alt 속성을 확인하죠. 따라서, alt 속성을 지정하지 않으면 이미지를 그냥 무시하고 지나갑니다.
  • 이런 이유로, 이미지에 alt 속성을 넣는 것은 시각 장애인 분들에게 친화적인 사이트가 됩니다.

결론

  • Service Worker 에 대해 더 알고 싶으시면 여기를 참고하세요!
  • 빠르고 사용자 친화적인 웹 사이트에 대해 더 알고 싶으면 이 책을 참고하세요.
Advertisements

Service Worker Introduction II

Service Worker 실행하기 위한 환경

  • 지원되는 브라우저 : Chrome 46 ↑, Firefox, Opera, Safari (지원예정)
  • 브라우저 지원에 대한 상세한 내용은 여기 참고
  • HTTPS 통신이 가능한 서버에서만 동작한다 (테스트를 위한 localhost 제외)

Service Worker 등록하기

  • 아래 코드처럼 javascript로 서비스워커를 등록한다.
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function(registration) {
    // Registration was successful
    console.log('ServiceWorker registration successful with scope: ', registration.scope);
  }).catch(function(err) {
    // registration failed 😦
    console.log('ServiceWorker registration failed: ', err);
  });
}
  • 위 코드를 살펴보면, 먼저 서비스워커 API 존재 유무를 파악한다.
  • 존재할 시에 /sw.js 파일을 등록한다.
  • 한가지 알아둘 것은, 페이지가 로딩될 때 마다 register() API를 호출해도 된다. 브라우저가 서비스워커 실행 유무를 판단하여 알아서 처리하기 떄문이다.
  • 서비스워커 파일은 도메인 루트에 위치한다. 예를 들어, 도메인이 /example/sw.js 인 경우, /example/로 시작하는 모든 도메인에 대하여 서비스워커가 실행된다.
  • 크롬브라우저 주소창에 chrome://inspect /#service-workers 입력하면 서비스워커 콘솔을 사용할 수 있다.
  • 등록된 서비스워커는 chrome://serviceworker-internals 을 통해 확인 및 관리 할 수 있다.

Service Worker 설치하기

  • 위 등록 절차를 거쳤다면, 이젠 서비스워커에서 사용할 자원들을 설치할 차례다.
  • 아래 코드를 이용하여 어떤 파일들을 캐싱할 것인지 결정한다.
self.addEventListener('install', function(event) {
  // Perform install steps
});
  • install 콜백 함수 안에 다음 3가지 순서를 추가한다.
    1. [열기] cache 열기
    2. [캐싱] 사용할 파일들 캐싱하기
    3. [확인] 해당 파일들이 모두 캐싱 되었는지 확인
var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});
  • caches.open()에 정의한 캐쉬명 변수를 이용하여 캐쉬 사용을 시작한다.
  • 다음 cache.addAll()를 이용하여 array 안에 선언한 필요파일들을 모두 캐싱한다.
  • event.waitUntil() 메서드로 설치가 얼마나 오래 걸리던 간에, 설치 후에 해당 이벤트를 수행할 수 있도록 한다. (Javascript Promise 사용됨)
  • 결론적으로, 모든 파일들이 성공적으로 캐싱되면 서비스워커가 정상적으로 설치된다.
  • 주의할 점은, 여기서 한 개의 파일이라도 캐싱에 실패할 경우 인스톨 전체의 프로세스가 종료된다는 것이다. 따라서 신중하게 캐싱할 파일 리스트를 정한다.

Cache 와 Return 요청

  • 서비스워커 설치까지 완료했다면, 이제 캐쉬된 결과를 받아볼 차례다
  • 설치 완료후에 페이지 이동이나 갱신을 하게 되면, 서비스워커는 fetch라는 이벤트를 수행하게 된다.
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
    )
  );
});
  • caches.match()는 들어오는 request에 대해서 서비스워커가 생성한 캐쉬가 있는지 확인한다
  • 만약 생성된 캐쉬가 있다면, 캐쉬 값을 리턴한다. 그렇지 않은 경우에는 fetch() 를 콜한다.
  • fetch() 네트워크로부터 받을 데이터가 있다면, 네트워크 요청을 보내 해당 데이터를 받는다.
  • 네트워크 요청을 각각 캐쉬로 저장하고 싶다면, 아래와 같은 형식으로 구현하면 된다.
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }

        // IMPORTANT: Clone the request. A request is a stream and
        // can only be consumed once. Since we are consuming this
        // once by cache and once by the browser for fetch, we need
        // to clone the response.
        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(
          function(response) {
            // Check if we received a valid response
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // IMPORTANT: Clone the response. A response is a stream
            // and because we want the browser to consume the response
            // as well as the cache consuming the response, we need
            // to clone it so we have two streams.
            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});

위 코드의 로직은 다음과 같다.

  1. fetch reqeust에 대해 promise (then) 콜백 호출
  2. 받은 response에 대하여 아래와 같은 절차를 수행
    • response 유효성 검사
    • status 값이 200인지 확인
    • type 값이 basic인지 확인 (our origin 인지 확인 / 3rd party 자원은 캐쉬가 되지 않는다는 의미)
  3. 위 절차들을 통과하면, response를 복제한다. 이유는 response는 Stream 방식이기 떄문에, body는 오직 한번만 실행 가능하다. 캐쉬 후에 브라우저에도 response를 던져야 하기 때문에, 복제를 해서 한개는 캐쉬를 한개는 브라우저에 각각 사용한다.

Service Worker 업데이트 하기

  • 서비스워커를 업데이트 해야하는 시점에서의 작업 절차는 다음과 같다.
    1. 서비스워커를 업데이트 하라. 사용자가 사이트를 네비게이팅 할 떄, 서비스워커 파일이 1byte라도 다를 경우 새로운 서비스워커로 간주한다.
    2. 새로운 서비스 워커가 시작되고, install 이벤트가 발생한다.
    3. 이 시점에서, 예전에 등록된 서비스워커가 현재 페이지를 제어하고 있기 때문에 새로운 서비스 워커는 waiting 상태로 진입한다.
    4. 사이트에서 열려있었던 페이지가 닫히면, 이전 서비스워커는 종료되고 새로운 서비스워커가 제어를 넘겨받는다
    5. 새로운 서비스워커로 제어가 넘어오면, activate 이벤트가 발생된다.
  • activate 콜백에서 발생하는 가장 흔한 작업은 cache management이다.
  • 이유는 바로 install 단계에서 이전 캐쉬를 다 지우게 된다면, 현재 페이지의 제어를 담당하는 old 서비스워커(현재 페이지에서 사용되고 있는 서비스워커 : 새로운 서비스워커에 비교해 old로 간주)의 경우 캐쉬에서 파일을 제공할 수가 없기 때문이다.
  • 예를 들어, my-site-cache-v1 캐쉬라는 파일이 있다고 가정하자. 그리고 이 캐쉬를 한개는 페이지에 한개는 블로그 포스트에 사용한다고 하자.
  • 이 의미는 install 단계에서 pages-cache-v1blog-posts-cache-v1 라는 두개의 캐쉬를 생성하고, 기존의 my-site-cache-v1 캐쉬는 지운다 는 것이다.
  • 아래의 코드를 확인해보면, cacheWhitelist에 존재하지 않는 캐쉬는 모두 서비스워커에서 삭제한다.
self.addEventListener('activate', function(event) {

  var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Rough Edges and Gotchas (현재 떠안고 있는 문제들)

서비스워커에는 현재 다음 두가지 이슈가 있다. 설치 실패 여부 확인 어려움, fetch() 디폴트 값

  1. 설치 실패 여부 확인 어려움
    • 서비스워커가 등록이 되더라도, chrome://inspect/#service-workerschrome://serviceworker-internals 로 확인하기 어렵다.
    • 따라서, chrome://serviceworker-internals 에서 Open DevTools window and pause JavaScript execution on service worker startup for debugging.를 체크하여 install event에 디버깅 문구를 넣어 확인한다.
  2. fetch() 디폴트 값
    • No Credentials by Default : fetch() 이벤트를 default로 사용시에는 쿠키 같은 credential들을 포함하지 않는다. 만약 credential을 포함하고 싶다면 아래와 같이
    fetch(url, {
    credentials: 'include'
    })
    
    • Non-CORS Fail by Default : 3rd party URL을 통한 자원 획득은 허용되지 않는다(CORS 지원하지 않는다면). 만약 CORS를 지원하려면 no-CORS 옵션을 추가한다. 하지만 이 방법은 opaque 응답을 야기하는데, 받은 응답이 성공인지 실패인지 확인할 수 가 없는 단점이 있다.
    cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
    return new Request(urlToPrefetch, { mode: 'no-cors' });
    })).then(function() {
    console.log('All resources have been fetched and cached.');
    });
    

Service Worker Introduction I

Service Worker 란 무엇인가

  • Rich offline experiences, periodic background syncs, push notifications 등 네이티브 앱에서만 가능했던 기능들을 웹에서 사용할 수 있도록 지원하는 스크립트
  • 웹 페이지와는 별개로 브라우저의 백그라운드에서 수행되는 스크립트
  • 오늘 기준으로 push notificationsbackground sync 를 지원한다.
  • 오프라인 사용에 대한 완벽한 지원을 한다
  • Service Worker 이전에는 SPA의 AppCache 와 같은 기능들이 존재 했었지만, multiple page에 대한 지원이 되지 않았다.

Service Worker 를 통해 할 수 없는 것들은?

  • Javascript Worker 이기에 DOM 에 직접 접근이 불가. (하지만 원하면 postMessage 와의 인터페이스를 통해 접근 가능)
  • 프로그래밍 가능한 네트워크 프록시이기 떄문에, 페이지 핸들링에 관련된 네트워크 요청을 제어할 수 있다.
  • 사용하지 않을 때는 종료된다. 따라서, onfetch & onmessage 핸들러를 통한 global state에 접근이 불가능 하지만, 원한다면 IndexedDB API를 이용하여 상태를 보존할 수 있다.

Service Worker Lifecycle

  • 웹 페이지와 완전 별개의 라이프싸이클을 갖고 있다.
  • [등록] 서비스워커 사용을 위해서는 먼저 페이지의 자바스크립트를 사용하여 등록해야 한다.
  • [설치] 설치하는 과정에서 static한 자원들을 캐싱하고, 캐싱이 완료되면 서비스 워커가 설치가 된다. 한 개의 파일일라도 캐싱에 실패하면, 설치가 종료되고, 서비스워커는 다시 활성화되지 않는다.
  • [활성] 설치가 되고 나면, 활성 스텝으로 넘어오고, 이 떄 이전(오래된) 캐쉬들을 다룰 수 있는 상태가 된다.
  • [제어] 활성화 스텝 이후에는 서비스 워커가 본격적으로 모든 페이지를 제어하기 시작한다. 서비스워커에게 제어권이 돌아가면, 보통 아래 2가지 상태(Fetch, Terminated)로 나뉘게 된다.
  • [페치/메시지] 네트워크 요청을 받거나 메시지를 페이지로부터 전달받았을 때 데이터를 fetch하거나 메시지 이벤트를 처리한다
  • [종료] 메모리 효율을 위해 서비스워커를 종료한다

Service Worker Overview Imagesw-lifecycle

Service Worker Reference