Rev. 2.73

svg-logo.png

푸하하! 결국에는 SVG 로고홈페이지에 적용했습니다. 위 화면은 적용후 파이어폭스 5에서 300% 확대하여 캡쳐한 화면입니다. SVG지원 여부를 판단하기 위한 스크립트를 서너개 찾았는데 그 중에서도 애플에서 사용한 스크립트가 가장 효용성이 좋기 때문에 이를 참조했습니다. 이미지 요소를 생성하여 src에 Data URI를 대입하고 응답여부로 사용가능 여부를 판단하는 원리입니다. 그렇기 때문에 callback을 받아 비동기식으로 결과를 처리해야하며, 응답속도는 onload 발생 시점으로 느리게 반응한다는 단점이 있습니다. 한번만 수행하면 다음 호출 부터는 동기식으로 사용여부를 반환합니다. 예전에 시도했던 방법과 원리는 같은데 왠지 더 좋아 보이네요.

/**
 * detect SVG support in browser
 *
 * @param {Function} callback for detecting SVG support
 * @return {Boolean} SVG supported
 *
 */
detectSVG.support = null;
function detectSVG(callback) {
  if (detectSVG.support === null) {
    var img = document.createElement("img")
      , onload = function () {
      detectSVG.support = true;
      if (typeof(callback) == "function") callback();
    };

    img.setAttribute("src", "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNzUiIGhlaWdodD0iMjc1Ij48L3N2Zz4%3D");
    if (img.complete) {
      img.style.visibility = "hidden";
      img.style.position = "absolute";
      document.body.appendChild(img);
      window.setTimeout(function() {
        detectSVG.support = false;
        document.body.removeChild(img);
        if (img.width >= 100) onload();
      }, 1);
    } else {
      detectSVG.support = false;
      img.onload = onload;
    }
  } else if (detectSVG.support && typeof(callback) == "function") callback();

  return detectSVG.support;
}

Comments

최근 홈페이지 리뉴얼 프로젝트를 중간에 인수받아 우여곡절 끝에 마무리하고 성공적으로 오픈했습니다. 안정화 기간이라서 여러 개선/교정/변경 사항들이 속출하고 있는 가운데 참 재미있는 항목이 하나 있었습니다. "애플의 홈페이지는 로고와 메뉴가 아무리 확대해도 깨지지 않는다. 우리도 그렇게 하는 것이 좋지 않겠냐?"라는 것이었죠. "어! 진짜?" 하고 즉시 살펴 보았습니다. 정말로 그렇더군요. 헐~

글 제목에서 눈치채셨겠지만 애플은 HTML5에서 공식으로 지원하는 미디어 포맷인 SVG(Scalable Vector Graphics)를 이용하여 로고와, 메뉴 텍스트를 CSS의 background 속성으로 표시하고 있었습니다. 아시다시피, 이 SVG라는 것을 사용하면 아무리 확대해도 깨지지 않는 그래픽 영역을 웹페이지에 삽입할 수 있습니다. 만약 SVG를 지원하지 않는 경우 PNG파일로 대체되며 확대하면 픽셀이 선명하지 않는 현상은 동일했습니다. PNG마저 지원하지 않는 경우(e.g. IE6)에는 GIF로 만들어진 이미지를 로드하도록 되어있습니다. 아무리 확대해도 깨지지 않는 메뉴와 로고라... 기발한 아이디어죠. 역시 애플 답다는 감탄사가 절로 터져나오더군요. 흥미가 생겨 조금더 파해쳐 보기로 했습니다. 이것이 언제 반영되었는지 알아내려고 각 사이트 첫 페이지의 모습을 날짜별로 담고 당시의 모습을 재현해 주는 서비스인 archive.org에서 검색해 보니 가장최근 크롤링된 자료가 2010년 12월 말경이였고 분명히 이미지만으로 메뉴가 구성되어있던 것을 확인하였습니다. 원래부터 SVG는 아니였고 최근 6개월 사이 반영된 내용임을 유추할 수 있습니다. 현재 SVG는 IE9를 비롯한 모든 메이저급 브라우저들이 지원하고 있습니다. 편의를 위해 애플 홈페이지의 메인메뉴 네비게이션 영역을 여기에 가져왔습니다.

SVG로 랜더링 된 애플 홈페이지 네비게이션 메뉴

이미지로 랜더링 된 애플 홈페이지 네비게이션 메뉴

또 한가지 재미있는 사실은 "enhanced.css"라는 스타일시트가 있는데, 이 녀석이 하는 역할은 기존에 이미지로 지정되어있던 background 속성이 전부 base64로 인코딩된 Data URI 스키마로 호출을 대체하거나 CSS3의 gradient 값으로 변경합니다. globalnav.js 자바스크립트 파일에서 사용가능 여부를 판단하여 CSS3와 Data URI를 지원하는 브라우저에서만 호출되도록 처리되어 있습니다. 이것은 CSS에 정적인 이미지들을 문자열 데이터로 정의함으로써 물리적인 파일들의 호출횟수를 줄여 웹사이트의 전반적인 성능을 향상시키는 방법입니다. 이 CSS파일에 정의된 마임-타입(Mime-Type)을 살펴보면 "image/svg+xml"도 찾아볼 수 있습니다. 본 주제와는 상관없는 것이므로 위 예제는 이미지를 사용하는 navigation.css만을 로드한 것입니다.

위 두 예제의 차이점은 첫 번째가 SVG 포맷으로 텍스트를 랜더링한 것이고 두 번째는 이미지 포맷으로 랜더링한 것입니다. nav 요소에 "svg"라는 클래스 이름의 포함여부로 구분하게 되어있습니다. "svg"라는 클래스명을 추가하는 것 역시 globalnav.js에서 구분하여 수행합니다. 그럼 이제부터 본격적으로 두 녀석의 차이점을 살펴보겠습니다.

chrome.png
위 그림은 최신 크롬 브라우저(14.0.825.0 dev-m)에서 300% 확대(최대치)한 후 캡쳐한 것입니다. IE9나 사파리(5.1, 모바일 포함) 역시 이와 유사한 결과를 출력합니다. 그러나 파이어폭스(5.0)는 SVG 미디어를 배경이미지로 사용한 경우에 이미지로 사용했을 때와 별반 다르지 않은 결과를 나타냈습니다. SVG 파일이 로드되었는데도 말예요. 브라우저의 버그로 보입니다만, 동일한 SVG 파일을 일반 이미지 요소에 "src" 속성으로 정의한 경우(<img src="nav.svg">) 별 문제없이(확대해도 깨지지 않는) 잘 나타났습니다. 아래 그림은 파이어폭스에서 300% 확대한 후 캡쳐한 화면입니다.(위 예제를 해당 브라우저에서 직접 확인해 보시길 권장합니다.)

firefox.png

파이어폭스의 버그를 꼬집고자하는 의도는 아닙니다만, 이러한 현상들이 있음을 확인하였습니다. 마지막으로 효율성에 대하여 살짝 집고 넘어가겠습니다. 이미지로 만들어진 텍스트 파일의 용량은 gif 포맷이 2.58 KB이고, png 포맷은 7.28 KB 입니다. 그렇다면, svg 포맷은? 무려 53.59 KB입니다. 이 크기는 왠만한 자바스크립트 라이브러리(jquery.min.js 31 KB)보다도 큰 용량입니다. 실제로 애플의 홈페이지를 로드할 때 메뉴 텍스트가 느리게 그려지는 현상을 실감할 수 있습니다. Firebug의 Network 콘솔을 확인해 보면 SVG파일이 가장 늦게 로드되는 것도 확인할 수 있습니다. 그럼에도 애플은 왜 이러한 선택을 한 것일까요? 그 해답은 애플만이 알겠지요...

자, 이제 본론입니다. 뭐 결론은 "애플이니까 한거다. 나도 저런거 만드는 애플이 부럽다." 정도로 답변했지만, 사실 이러한 요구사항이 들어올 줄은 꿈에도 생각못해 다소 당혹스럽습니다. "애플은 워낙에 확대/축소가 자유로운 장치와 OS를 공급하다 보니 만든거다"며 변명을 하자니, 일하기 싫어하는 놈 같고 무턱대고 적용하자니, 디자이너님 모시고 SVG 설명 드리고 배경과 텍스트 분리해서 좌표에 딱 드러맞게 저장해서 주세요. 라 했을 때 "100% 이상으로 확대해서 보는 일부 사용자를 위해 이렇게까지 해야하나요?"라는 질문이 돌아온다면 딱히 떠오르는 대답도 없는 실정입니다. 여러분이라면 어떻게 하시겠어요?

덧. 참 오랜만에 블로그를 갱신했네요. 요즘 제 상태가 블로깅은 사치입니다. 쩝.

Comments

CSS를 이용한 복잡한 페이지 디자인과 Javascript를 이용한 동적변화가 매우 다양하게 이용되고 있는 상황에서 이에 따른 속도저하 등의 문제점이 발생하고 있다. 이를 원천적으로 해결할 수는 없겠으나 조금이라도 줄일 수 있는 방법들을 찾기 위해 브라우저의 작동원리를 이해해보고, 그에 따른 문제해결 방법을 찾아보고자 한다.

브라우저 렌더링 프로세스의 이해

reflow-1.png

- User Interface - 주소창, 뒤로가기/앞으로가기 버튼, 즐겨찾기 기능등을 포함하며 브라우저 중 웹페이지
표시부분(document)을 제외한 거의 모든 부분에 해당.
- Browser Engine - 렌더링 엔진에 질의를 보내며, 조작하는 인터페이스
- Rendering Engine - 요청된 콘텐츠를 화면에 뿌려주는 기능을 담당함. (전송된 HTML과 CSS 등을 파싱하여 디스플레이)
- Networking - HTTP 리퀘스트와 같은 네트워크 통신기능 수행. 
- UI Backend - 브라우처 창의 형태나 셀렉트버튼, 체크박스 등을 표현함. OS의 UI 메소드에 의존함. (XP에서의 셀렉트박스와 윈도우7에서의 셀렉트박스가 다른 것을 생각하면 이해가 쉬움)
- Javascript Interpreter - 자바스크립트 코드의 파싱과 실행에 사용 (유명한 것이 바로 Chrome의 V8 엔진)
- Data Storage - 지속적인 계층(쿠키 등을 위한 저장공간). HTML5에서는 웹DB가 해당됨.

Rendering engine basic flow

브라우저가 네트워크 계층에서 요청된 데이타를 받아오면 렌더링 엔진이 움직이기 시작한다. 다음은 렌더링 엔진의 기본적인 흐름을 도식화 한 것

reflow-2.png

1. Parsing HTML : HTML을 파싱하고 DOM Tree를 설계
2. Render Tree
3. Layout : 각각의 노드가 화면내에 위치되어야 할 좌표값 계산 (화면 내 position과 size) 후 배치
4. Paint : *계산되고 지정된 명령에 따라 각각의 노드를 그림

화면구성이 완료된 후 동적인 변화(JS를 통한 DOM 편집, 스타일 수정 등) 발생 시엔?

어떠한 변화가 발생했을 때 브라우저는 최소한의 대응을 하도록 설계되었으며 만일 특정 엘리먼트의 color값에 변화가 발생한다면 오직 해당엘리먼트의 repaint만을 유발한다. 하지만 엘리먼트의 포지션에 변화가 발생했을 경우에는 해당 엘리먼트의 Repaint는 물론 레이아웃까지도 유발(Reflow)한다. html 엘리먼트의 폰트사이즈를 키우는 것과 같은 커다란 변화들은 전체 Render Tree의 Repaint와 Reflow를 유발시킨다.

Reflow? Repaint(or Redraw)

엘리먼트의 스킨에 변화가 발생하지만, 레이아웃에는 영향을 미치지 않을 때 유발된다. (visibility, outline, background-color 등이 포함) Opera에 따르면 Repaint는 해당 행위가 발생하는 순간, 문서내 DOM tree의 다른 노드들의 스킨까지도 검증해야 하므로 비용이 높다고 함.

Reflow

문서 내 노드들의 레이아웃, 포지션을 재계산 후 다시 뿌려주므로 Repaint보다도 더 심각한 퍼포먼스 저하를 유발시키는 프로세스이다. 특정 엘리먼트에 대한 Reflow 발생 시, 페이지에서의 해당 요소는 즉시 Reflow State가 되며 해당 엘리먼트의 자식요소와 부모/조상 요소역시 레이아웃 계산을 진행한다. _(결국은 페이지 전체를 다시 훑는 것이나 마찬가지) _Opera에 의하면, 대부분의 리플로우는 페이지 전체의 렌더링을 다시 일으킨다고 한다.

" Reflows are very expensive in terms of performance, and is one of the main causes of slow DOM scripts, especially on devices with low processing power, such as phones.In many cases, they are equivalent to laying out the entire page again." 
- Reflow는 퍼포먼스 측면에서 매우 고비용을 발생시키는 프로세스이며, 휴대전화와 같은 저성능 디바이스에서는 특히나 더욱 느린 DOM 스크립팅을 발생시키는 주범이다. 많은 경우에서 Reflow는 페이지 전체를 다시한번 레이아웃시키는 결과를 가져온다.

무엇이 Reflow를 유발시키는가?

특정 엘리먼트에 스타일변화가 발생했을 때, 그 개체가 가진 자식요소에 대한 레이아웃 재정리를 위해 Reflow가 실행된다. 설령 그 변화가 그 자식요소 및 페이지에는 아무 영향을 미치지 않을지라도, 기계는 이를 미리 알고있지 못한다. 따라서 작은 변화에도 자식개체는 물론, 페이지 전체에 Reflow가 실행된다. Mozilla에 따르면 다음의 케이스에서 Reflow가 발생한다고 한다.
- 윈도우 리사이징
- 폰트의 변화
- 스타일 추가 또는 제거
- 내용 변화 (인풋박스에 텍스트 입력 등..)
- :hover와 같은 CSS Pseudo Class
- 클래스 Attribute의 동적 변화
- JS를 통한 DOM 동적 변화
- 엘리먼트에 대한 offsetWidth / offsetHeight (화면에서 보여지는 좌표) 계산시
- 스타일 Attribute 동적변화

Reflow를 피하거나 그 영향을 최소화하는 방법

1. 클래스 변화에 따른 스타일 변화를 원할 경우, 최대한 DOM 구조 상 끝단에 2. 위치한 노드에 주어라. 
3. 인라인 스타일을 최대한 배제하라.
4. 애니메이션이 들어간 엘리먼트는 가급적 position:fixed 또는 position:absolute 로 지정
5. 퀄리티와 퍼포먼스 사이에서 타협하라
6. 테이블 레이아웃을 피하라
7. IE의 경우, CSS에서의 JS표현식을 피하라.
8. JS를 통해 스타일변화를 주어야 할 경우, 가급적 한번에 처리하라.
9. CSS Rules는 필요한 만큼만 정리하라.
10. position:relative 사용 시 주의하자.

1. 클래스 변화에 따른 스타일 변화를 원할 경우, 최대한 DOM 구조 상 끝단에 위치한 노드에 주어라.

클래스 변화로 인한 Reflow는 물론 피할 수 없겠지만,  그 효과는 줄일 수 있다. DOM 트리에서 가급적 말단에 위치한 노드에 클래스 변화를 줄 경우, 이는 리플로우의 행동반경을 전체 페이지가 아닌 일부 노드들로 제한할 수 있다. 따라서 전체 페이지를 감싸는 wrapper에 클래스를 수정하는 행위는 꼭 피해야 한다. 또한 OOCSS 방식을 통해 클래스변화가 발생할 경우, 특정 엘리먼트에 대해 상당히 많은 클래스를적용시키는 것 같지만, 실제로는 리플로우의 영향을 최소화함으로써 퍼포먼스적인 측면에서 큰 이득이 발생한다.

2. 인라인 스타일을 최대한 배제하라.

DOM은 매우 느린 구조체이다. 게다가 인라인상에 스타일이 주어진 경우, 리플로우는 페이지 전체에 걸쳐 수차례 발생하게 된다. 만일 인라인스타일이 없을 경우, 외부스타일 클래스의 조합으로 단 한번만 리플로우를 발생시킨다.

3. 애니메이션이 들어간 엘리먼트는 가급적 position:fixed 또는 position:absolute 로 지정

일반적으로 JS (특히 jQuery)나 CSS3로 width/height 또는 위치이동을 구현한 애니메이션은 거의 초단위로 상당한 Reflow를 불러일으킨다. 이러한 경우에 해당 개체의 position 속성을 fixed 또는 absoute로 주게 되면 다른 요소들의 레이아웃에 영향을 끼치지 않으므로 페이지 전체의 Reflow 대신 해당 애니메이션요소의 Repaint만을 유발한다. 이것은 비용적인 측면에서 매우 효율적인 방법이다.

4. 퀄리티와 퍼포먼스 사이에서 타협하라

한 time에 1px을 움직이는 애니메이션 A와 한 time에 3px를 움직이는 애니메이션 B가 있다고 할 때, 애니메이션의 계산과 페이지 Reflow 계산이 동시다발적으로 발생함으로써 CPU 퍼포먼스 비용이 발생하는데, A가 B에 비해 더욱 큰 비용이 발생한다. 속도가 빠른 디바이스에서는 둘다 비슷하게 보이지만, 속도가 느린 (휴대전화와 같은) 디바이스에서는 그 차이가 눈에 띌 수 있다. 

5. 테이블 레이아웃을 피하라

테이블로 구성된 페이지 레이아웃은 점진적(progressive) 페이지렌더링이 적용되지 않으며, 모두 로드되고 계산된 후에야 화면에 뿌려진다. 더군다나 Mozilla에 따르면 테이블 레이아웃에서는 아주 작은 변화마저도 해당 테이블 전체 모든 노드에 대한 Reflow를 발생시킨다고 한다. 또한 YUI data table 위젯의 개발자인 Jenny Donnelly 에 의하면, 레이아웃 용도가 아닌 데이터표시 용도의 올바른 테이블이라 할지라도 해당 테이블에 table-layout:fixed 속성을 주는 것이 디폴트값인 auto에 비해 성능면에서 더 좋다고 한다.

6. IE의 경우, CSS에서의 JS표현식을 피하라.

소개된지 오래된 규칙이지만 매우 효과적인 규칙이다. 이 CSS 표현식의 비용이 매우 높은 이유는, 문서 전체 또는 문서중 일부가 Reflow될 때마다 표현식이 다시계산되기 때문이다. 이는 결국.. 애니메이션과 같은 변화에 의해 리플로우가 발생했을 때, 경우에 따라 1초당 수천, 수만번의 표현식 계산이 진행될 수 있다는 것을 의미한다. 때문에 CSS표현식은 반드시 피해야한다.

7. JS를 통해 스타일변화를 주어야 할 경우, 가급적 한번에 처리하라

특정 요소에 스타일변화를 주어야 할 경우 다음과 같이 시도해볼 수 있다.

var toChange = document.getElementById('elem');
toChange.style.background = '#333';
toChange.style.color = '#fff';
toChange.style.border = '1px solid #ccc';

이러한 접근은 여러번 중복된 Reflow와 Repaint를 유발시킨다. 때문에 위와 같은 방법보다는 다음과 같은 방법으로, 단 한번의 변화만을 발생시키는 것이 더욱 효과적이다.

/* CSS */
#elem { border:1px solid #000; color:#000; background:#ddd; }
.highlight { border-color:#00f; color:#fff; background:#333; }
/* js */
document.getElementById('elem').className = 'highlight';

8. CSS 하위선택자는 필요한 만큼만 정리하라.

Reflow 자체보다도, Reflow가 유발시키는 CSS Recalculation 에 필요한 내용이다. CSS의 Rule 매칭 프로세스는, 가장 우측의 핵심 선택자에서 좌측으로 흐른다. 이 과정은 더이상 매치시킬 Rule이 없거나 잘못된 Rule이 튀어나올 때까지 계속 매칭시키며 진행된다. 만일 해당 CSS의 특별성(specialty)이 확보되는 선에서, 가급적 딱 필요한만큼만 사용한다면 퍼포먼스 측면에서의 극적인 향상이 발생하게 된다. (즉, 룰이 적을수록 비용절감) 설령 .btn_more라는 클래스가 list_service 내에 쓰이는 유일한  요소일 경우, 아래와 같은 두가지 예가 발생할 수 있다.

첫번째 예:

.section_service .list_service li .box_name .btn_more {
  display:block;width:100px;height:30px;
}

두번째 예:

.section_service .list_service .btn_more {
  display:block;width:100px;height:30px;
}

가정 상 둘 다 .btn_more의 specialty가 유효한 CSS임에도 불구하고, 첫번째 예처럼 쓰는 경우는 "코드 가독성"과 같은 이유에서일 것이다. 유지보수의 측면에서 보자면 물론 가독성도 중요한 부분이나, 위의 첫번째 예와 같이 5단계에 걸쳐 필요이상의 규칙들을 작성해놓을 경우 퍼포먼스 하락을 가져올 수 있다. 더군다나 이러한 CSS 코드들이 5~10라인이 아닌, 500~1000라인쯤 될 경우 퍼포먼스에 상당한 영향을 미치게 된다. 때문에 두번째 예와 같이 딱 필요한 선에서 핵심만을 짚는 CSS Rule 선언이 필요하며, 코드 가독성을 위해서라면 차라리 해당 분류 묶음에 CSS 주석처리를 하는 것이 효과적이다. (하위선택자는 가급적 적을수록 좋다)

9. position:relative 사용 시 주의하자.

페이지를 새로 열거나 Reflow가 발생되어 CSS Calculation이 발생할 경우, Box model Calculation → Normal Flow 의 순서로 계산이 진행된다. (Normal flow는 Layout 또는 Reflow라 불리는 과정에 속하는 일부임.) 일반적인 경우, 엘리먼트 들은 margin, border, padding, content(width,height) 등 Box model을 먼저 계산한 후 Normal flow 상태의 레이아웃에 배치된다. (다른말로 선형적 배치)

1) Box model Calculation에 의한 계산
아래 이미지와 같이, 화면내 배치가 아닌 각 엘리먼트 자체의 Metrics 계산을 우선 진행한다.
reflow-3.jpg

2) Normal flow에 의해 선형적으로 배치된 상태
Box model 계산 후, 마크업 순서에 따라 화면 내 배치를 실행한다. (단, position:absolute 또는 fixed일 경우 Normal flow를 거치지 않고 Out of flow 즉, 곧바로 Positioning을 진행한다.)
reflow-4.png

3) Normal Flow 이후
Float냐 Position이냐에 따라 Positioning 과정이 한번 더 일어나는데 각 케이스별 시나리오는 다음과 같다

→ case 1. Float 속성을 가진 요소
Normal flow 이후 별도의 Positioning 계산은 없으며, 왼쪽 또는 오른쪽으로 자신이 갈수있는 한 끝까지 이동한다. (즉, Box model → Normal flow → Floating)
reflow-5.png

→ caes 2. position:relative;와 함께 top,left 등 위치값을 가진 요소
Normal flow 상태에서 한번 더 Positioning 프로세스를 거치게 된다. (Box model → Normal flow → Positioning)
reflow-6.png

→ case 3. position:absolute 또는 fixed를 가진 요소
Box model 계산 후 Normal flow 과정을 거치지 않고 바로 자신의 위치에 박히게 된다. (Out of flow) (Box model → Positioning)
reflow-7.png

위에서 확인할 수 있듯, position:relative가 오히려 position:absolute 또는 float 속성보다 더 큰 비용을 가진다. (Box model → Normal flow → Positioning 의 3단계를 모두 거치므로) 때문에 UL 또는 OL과 같은 목록에서 상당수 반복되는 LI 요소에 position:relative 와 top,left 속성등을 주는 경우, 퍼포먼스 하락이 발생할 가능성이 크다. 확인해볼 수 있는 URL : http://211.233.30.20/~bjs/reflow.html

관련 참조자료 및 인용자료

- How browsers work - behind the scenes of modern web browsers (by Tali Garsiel)
- Reflows & Repaints : CSS Performance making your Javascript slow? (by Nicole Sullivan)
- Efficient Javascript (by Mark Wilton-jones, Opera)
- Pegs, Holes And Reflow (by Robert O'Callahan, Mozilla)
- Notes on HTML Reflow (by Chris Waterson, Mozilla)
- Gecko:Reflow Refactoring (Mozilla Wiki)
- Writing Efficient CSS for use in the Mozilla UI (by David Hyatt)
- WebCore Rendering I - The Basics (by David Hyatt)
- CSS Positioning and Layout (by Jennifer Kyrnin)
- The CSS Box Model (by Jennifer Kyrnin)

작성자 : 조규태 (다음서비스 웹표준FT 3파트)

Comments