사용자 경험(UX)을 극대화하기 위해서는 웹 페이지가 얼마나 빠르고 원활하게 렌더링 되는지가 매우 중요합니다. 브라우저는 웹 페이지를 로드하고 사용자에게 시각적으로 표시하기 위해 여러 단계의 렌더링 과정을 거치는데, 이 과정 속에서 JavaScript를 통해 동적으로 DOM 요소의 스타일을 조작하여 사용자 인터페이스(UI)를 보다 유연하고 인터랙티브하게 만들 수 있습니다🧚
하지만, 이러한 DOM 요소 조작이 적절하지 못하게 이루어진다면 브라우저가 불필요하게 자원을 낭비하거나 서비스의 성능 저하가 초래될 수 있습니다. 이러한 상황을 방지하기 위해서는 페이지 내 요소들의 크기, 위치, 색상 등 시각적 속성을 변경할 때, 브라우저가 어떻게 이에 반응하고 반영하는지를 이해해야합니다. 스타일 속성 조작은 단순히 요소 외관을 변경하는 것을 넘어 페이지 전체의 렌더링 성능에도 직접적인 영향을 미칠 수 있거든요.
이번 포스트에서는 브라우저의 렌더링 과정이 어떻게 이루어지는지
, 그리고 JavaScript로 DOM 요소의 스타일을 조작할 때 브라우저가 어떤 과정을 거쳐 화면을 갱신하는지
를 살펴볼 것입니다. 또한, 직접적인 스타일 조작과 클래스 기반의 스타일 조작 방식이 각각 렌더링 성능에 어떤 영향을 미치는지 비교
하며 최적화된 스타일 변경을 위한 전략을 제시해보고자 합니다.
웹 페이지가 사용자에게 시각적으로 표시되기까지는 여러 단계의 렌더링 가정을 거치게 됩니다. 브라우저는 HTML, CSS, JS 파일을 받아들이고, 이를 기반으로 화면에 페이지를 그려내기 위한 일련의 작업을 수행합니다.
파싱(Parsing)
은 브라우저가 HTML과 CSS 파일을 읽고 해석하는 과정입니다. 브라우저는 HTML 문서를 읽어들여 DOM(Document Object Model)
트리를 생성합니다. DOM은 웹 페이지의 구조를 계층적으로 표현한 트리구조로, 각 HTML 요소가 DOM 트리의 노드로 표현됩니다.
동시에, 브라우저는 CSS 파일을 파싱하여 CSSOM(CSS Object Model)
트리를 생성합니다. CSSOM은 CSS 규칙을 계층적으로 구성한 트리로, 각 규칙이 어떤 요소에 적용되는지에 대한 정보를 포함하고 있습니다. 이 DOM과 CSSOM은 이후에 결합되어 브라우저가 화면에 실제로 표시할 내용을 결정하는 것에 중요한 역할을 합니다.
DOM과 CSSOM이 만들어지면 브라우저는 이 둘을 결합하여 렌더 트리(Render Tree)
를 생성합니다. 렌더 트리는 화면에 실제로 표시될 요소들만을 포함하는 트리 구조입니다. 예를 들어, display : none;
과 같은 스타일이 적용된 요소는 렌더 트리에서 제외됩니다.
렌더 트리의 각 노드는 요소의 스타일, 크기, 위치 등의 정보를 포함하며, 브라우저는 이 정보를 바탕으로 페이지의 레이아웃을 계산하게 됩니다.
렌더 트리가 완성되면, 브라우저는 각 요소의 정확한 위치와 크기를 계산하는 레이아웃(Layout)
단계를 거칩니다. 이 단계는 리플로우(Reflow)
라고도 불리며, 페이지의 각 요소가 화면에서 차지할 위치와 크기를 결정합니다.
레이아웃 과정에서 각 요소의 위치와 크기는 부모-자식 관계를 바탕으로 결정되며, 폰트 크기, 패딩, 마진, 보더 등의 CSS 속성이 이 과정에 영향을 미칩니다. 이를 통해 브라우저는 페이지의 구조와 요소 배치를 파악합니다.
레이아웃이 완료된 후, 브라우저는 각 요소를 실제 화면에 그리는 페인팅(Painting)
과정을 수행합니다. 페인팅은 브라우저가 요소를 시각적으로 표현하는 과정으로, 여기서 각 요소는 배경색, 텍스트, 이미지 등의 시각적 속성에 따라 픽셀 단위로 그려집니다. 페인팅 과정이 복잡하거나 많은 요소가 포함된 경우, 브라우저의 성능에 영향을 미칠 수 있습니다.
페인팅이 모두 이루어지면, 브라우저는 합성(Compositing)
단계를 통해 최종적으로 화면에 표시할 이미지를 만들어냅니다. 이 과정에서 여러 레이어로 분리된 요소들이 하나의 이미지로 통합됩니다. 그 중 애니메이션이나 복잡한 그래픽이 포함된 요소가 존재한다면, 이들은 별도의 레이어로 처리되어 합성 단계에서 최종적으로 결합됩니다. 이 과정을 빠르고 효율적으로 진행하기 위해 GPU(Graphics Processing Unit)이 활용되기도 합니다.
이 합성 단계까지 모두 수행하면 비로소 렌더링 과정이 마무리 되고, 웹 페이지가 사용자에게 표시됩니다.
가령 background-color
를 조정하는 등 DOM 요소의 스타일 속성을 변경하면, 브라우저는 이를 반영하기 위해 특정 렌더링 단계로 되돌아가 다시 실행합니다. 변경되는 스타일 속성에 따라 레이아웃
, 페인팅
, 합성
단계를 다시 거치게 됩니다.
요소의 위치나 크기
를 다루는 스타일 속성을 변경한다면, 브라우저는 해당 요소, 그리고 그와 관련된 다른 요소의 레이아웃을 다시 계산합니다.
요소의 크기를 변경하는 width
, height
, 요소의 외부 및 내부 간격을 변경하는 margin
, padding
, 요소의 위치를 변경하는 position
, 그리고 요소의 정확한 위치를 지정하는 top
, left
, bottom
등의 속성 변경은 레이아웃을 변경하게 만듭니다. 이러한 속성 변경은 브라우저가 레이아웃을 다시 계산하게 하고, 이를 바탕으로 페인팅을 새로 진행해야하므로 성능 부담이 큽니다.
페인팅에 영향을 미치는 스타일 속성은 요소의 시각적 표현만을 변경합니다. 이 경우 브라우저는 레이아웃을 다시 계산할 필요는 없지만, 화면에 표시되는 픽셀을 다시 그려야 합니다.
텍스트의 색상을 변경하는 color
, 요소의 배경색을 변경하는 background-color
, 요소의 테두리 스타일, 너비, 색상을 변경하는 border
, 요소에 그림자를 추가하거나 변경하는 box-shadow
등의 속성 변경은 페인팅을 다시 수행하게 만듭니다. 이러한 속성 변경은 레이아웃에는 영향을 미치지 않지만, 요소의 시각적 변경이 발생하므로 브라우저가 해당 부분을 다시 그려야 합니다. 페인팅은 리플로우보다는 덜 복잡하지만, 여전히 성능에 영향을 줄 수 있습니다.
요소를 회전, 확대, 축소, 이동 등 변환하는 transform
, 요소의 투명도를 변경하는 opacity
, 그리고 요소에 블러, 밝기 조절 등의 필터를 적용하는 filter
등은 합성 단계만으로 처리됩니다. 합성은 GPU를 사용하여 빠르게 처리되기 때문에, 성능에 비교적 덜 부담이 됩니다. 주로 애니메이션이나 인터랙션이 자주 발생하는 요소에 자주 적용됩니다.
이렇듯, 스타일 변경이 페이지의 렌더링에 어떤 영향을 미치는지 이해한다면 성능 최적화를 위해 보다 신중한 선택을 할 수 있습니다. 또한, 불필요한 레이아웃 변경을 피하고, 스타일 변경이 필요한 요소에 대해서만 페인팅이 이루어지도록 코드를 최적화할 수 있습니다.
이제 JavaScript에서 실제로 DOM 요소의 스타일을 어떻게 조작하는지, 그리고 이러한 조작이 브라우저의 렌더링 과정에 어떤 영향을 미치는지를 알아보려 합니다. JavaScript를 사용하여 DOM 요소의 스타일을 조작하는 방식에는 크게 두 가지가 있습니다. 직접 스타일 속성을 조작하는 방식
과 클래스를 추가하거나 제거하는 방식
입니다.
우선, JS에서 특정 DOM 요소의 style 객체에 직접 접근하여 개별 스타일 속성을 설정하는 element.style.property
를 사용하여 개별적인 CSS 속성을 조작할 수 있습니다.
noHistoryMessage.style.color = '#A0A3BD';
noHistoryMessage.style.textAlign = 'center';
noHistoryMessage.style.marginTop = '20px';
브라우저는 style 속성을 변경할 때마다 이를 감지합니다. 위의 코드에서 color
, textAlign
, marginTop
과 같은 스타일 속성은 개별적으로 변경됩니다. 여러 스타일 속성을 각각 수정할 경우, 브라우저는 각 속성마다 스타일을 다시 계산하고, 필요하면 레이아웃을 재조정하며, 페인팅 과정을 반복합니다. 따라서 이러한 방식은 여러 속성이 자주 변경될 때 비효율적일 수 있습니다😓
이와 달리, classList
API를 사용하여 DOM 요소에 클래스를 추가하거나 제거하는 방식으로도 스타일 속성을 조정할 수 있습니다. 이 방식에서는 별도의 CSS 파일에 미리 정의된 CSS 클래스를 요소에 적용함으로써 한 번에 여러 스타일을 조정합니다.
noHistoryMessage.classList.add('no-history-message');
.no-history-message {
color: #A0A3BD;
text-align: center;
margin-top: 20px;
}
이러한 방식은 클래스에 의해 한 번에 여러 스타일 속성이 적용되기 때문에, 브라우저가 스타일을 계산하고 페인팅하는 과정이 더 효율적으로 이루어집니다. 그리고 동일한 클래스를 여러 요소에 적용할 때, 브라우저는 이미 계산된 스타일을 재사용할 수 있어 성능 이점이 있습니다.
또한 JS와 CSS 파일의 역할 분리가 더욱 명확히 이루어집니다. JS 파일에서는 스타일 관련 코드가 빠지게 되어 코드가 간결해집니다. 또한 스타일을 변경할 때엔 CSS 파일에서만 수정하면 되기에 유지 보수가 수월해집니다.
요약하자면, 직접 스타일 조작
은 특정 상황에서 유용하지만, 스타일 변경이 복잡해지면 코드가 지저분해지고 성능에 부담이 될 수 있습니다. 반면, 클래스 기반 스타일 조작
은 코드의 재사용성과 유지보수성을 높이고, 브라우저의 렌더링 성능을 최적화하는 데 유리합니다.
따라서, 가능하다면 클래스 기반의 스타일 조작을 주로 활용하고, 직접적인 스타일 변경은 필요한 경우에만 사용하는 것이 좋습니다.
이렇듯 JavaScript에서의 스타일 조작은 단순히 시각적인 변경을 위한 도구가 아니라, 브라우저의 렌더링 성능을 최적화할 수 있는 중요한 기법입니다. 그 영향력을 잘 이해하고 올바른 방식으로 스타일을 조작함으로써 더 나은 성능과 사용자 경험을 제공하는 웹 페이지를 개발해보아요 🦖