지금까지 브라우저가 문서를 읽어 나무 구조를 세우고, 스타일 정보를 또 다른 구조로 정리하는 과정을 살펴보았다. 이제 이 두 구조가 손을 맞잡고 실제로 화면을 그리기 위한 준비에 들어간다. 이번 편이 다루는 주제는 그 첫 단계, 곧 무엇을 그릴지 골라내고 그것들이 화면의 어디에 얼마만 한 크기로 놓일지를 계산하는 과정이다. 이 단계는 성능에서 특히 자주 문제를 일으키는 지점이다.
이 계산은 눈에 보이지 않지만, 사용자가 화면을 스크롤하거나 무언가를 조작할 때마다 끊임없이 다시 이뤄진다. 그래서 이 계산이 얼마나 자주, 얼마나 무겁게 일어나느냐가 화면의 부드러움을 좌우한다. 이번 편에서는 이 자리 잡기 계산이 어떻게 이뤄지고 왜 비싼지, 그리고 무심코 저지르는 어떤 실수가 이 비용을 폭발시키는지를 짚는다.
보이는 것만 골라낸 새 구조
화면을 그리려면 먼저 무엇을 그릴지를 정해야 한다. 브라우저는 문서 나무와 스타일 구조를 합쳐, 실제로 화면에 나타날 요소들만 골라낸 새로운 구조를 만든다. 이 새 구조에는 오직 눈에 보일 것들만 담기므로, 이후의 그리기 작업이 한결 간결해진다.
여기서 중요한 점은 문서 나무에 있다고 해서 모두 이 새 구조에 들어오는 것은 아니라는 사실이다. 스타일에서 아예 보이지 않게 지정된 요소는 자리조차 차지하지 않으므로 이 구조에서 제외된다. 존재하지만 그리지 않을 것들을 미리 걸러 내는 셈이다.
다만 보이지 않게 만드는 방식에는 미묘한 차이가 있다. 완전히 없는 것처럼 취급되어 자리도 차지하지 않는 경우가 있고, 눈에는 안 보이지만 자리는 그대로 차지하는 경우가 있다. 앞의 것은 이 새 구조에서 빠지지만, 뒤의 것은 자리를 계산해야 하므로 구조에 남는다. 이 차이는 실무에서 생각보다 중요하다. 무언가를 잠시 감출 때 어떤 방식을 쓰느냐에 따라, 주변 요소들이 그 빈자리를 메우며 화면이 출렁일 수도 있고 자리를 유지한 채 조용히 사라질 수도 있기 때문이다.
브라우저가 이렇게 보이는 것만 추려 내는 이유는 명확하다. 그릴 필요가 없는 것까지 자리를 계산하고 색을 칠하는 것은 낭비이기 때문이다. 그릴 대상을 먼저 좁혀 두면 그다음의 무거운 계산을 꼭 필요한 것에만 집중할 수 있다. 만약 이 단계에서 그리지 않아도 될 것까지 잔뜩 남겨 두면, 그 부담은 고스란히 뒤따르는 무거운 계산으로 넘어가 화면 전체를 느리게 만든다.
자리를 정하는 배치 계산
그릴 대상이 정해지면, 브라우저는 각 요소가 화면의 정확히 어디에 놓이고 얼마만 한 크기를 가질지를 계산한다. 이 자리 잡기 계산은 화면 전체를 하나의 좌표 위에 펼쳐 놓고, 요소 하나하나의 위치와 크기를 구체적인 값으로 확정하는 작업이다.
이 과정에서 브라우저는 비율로 적힌 값들을 실제 화면의 점 단위로 환산한다. 부모의 절반 크기라거나 화면 너비에 맞춘다는 식의 상대적인 지정이, 이 단계에서 비로소 구체적인 크기로 바뀐다. 그래서 같은 문서라도 화면 크기에 따라 배치 결과가 달라진다. 요소들이 서로 밀고 당기며 자리를 잡는 방식도 이 단계에서 결정된다. 어떤 요소는 나란히 늘어서고, 어떤 요소는 남는 공간을 나눠 가지며, 어떤 요소는 격자에 맞춰 배치된다.
중요한 점은 하나의 요소가 자리를 잡으면 그 결과가 주변에 연쇄적으로 영향을 준다는 것이다. 어떤 요소의 크기가 커지면 그 옆이나 아래의 요소들이 밀려나고, 그러면 또 그다음이 밀려나는 식으로 파장이 퍼진다. 그래서 작은 변화 하나가 넓은 범위의 재계산을 부를 수 있다.
이 배치 계산의 결과는 다음 단계인 색칠의 밑그림이 된다. 각 요소의 정확한 위치와 크기가 정해져야 브라우저가 그 자리에 색을 채워 넣을 수 있기 때문이다. 말하자면 이 단계는 화면이라는 그림의 윤곽선을 그리는 작업에 해당한다. 배치 계산은 렌더링 과정에서 가장 무거운 축에 든다. 골라내기나 색칠에 비해, 요소들의 상호 관계를 모두 고려해 자리를 확정하는 셈이 특히 부담이 크다.
배치 계산은 왜 비싼가
배치 계산이 비싼 첫 번째 이유는 그 연쇄성 때문이다. 앞서 말했듯 한 요소의 변화가 주변으로 퍼지므로, 겉보기에 작은 변경도 실제로는 넓은 영역의 재계산을 부른다. 그래서 비용을 요소 하나 기준으로 가늠하면 크게 과소평가하게 된다.
두 번째 이유는 이 계산이 화면이 바뀔 때마다 반복된다는 점이다. 사용자가 스크롤하거나 창 크기를 바꾸거나, 스크립트가 요소의 크기나 위치를 건드릴 때마다 브라우저는 영향받은 부분의 자리를 다시 잡는다. 한 번의 계산은 짧아도, 자주 반복되면 그 합은 결코 작지 않다. 세 번째 이유는 이 계산이 화면을 그리는 주된 흐름과 같은 자리에서 이뤄진다는 것이다.
이 흐름이 배치 계산에 오래 붙들리면 그동안 다른 작업이나 사용자 반응이 밀린다. 그래서 배치가 무거워지면 단지 화면이 늦게 그려지는 데 그치지 않고, 클릭이나 입력에 대한 반응까지 함께 둔해진다. 사용자가 버튼을 눌렀는데 한 박자 늦게 반응하는 답답함의 상당수가 바로 이 무거운 배치 계산이 흐름을 붙들고 있기 때문에 생긴다.
네 번째로, 화면에 요소가 많고 구조가 깊을수록 계산할 관계도 그만큼 늘어난다. 앞 편들에서 문서 나무를 얕고 간결하게 유지하라고 강조한 이유가 여기서도 이어진다. 불필요하게 겹겹이 감싼 구조는 배치 계산을 무겁게 만드는 숨은 원인이 된다. 이 모든 이유를 종합하면, 배치 계산의 비용을 줄이는 길은 두 갈래다. 하나는 계산할 대상 자체를 가볍게 유지하는 것이고, 다른 하나는 계산을 다시 하는 횟수를 줄이는 것이다.
배치를 강제로 반복시키는 실수
실무에서 가장 자주 마주치는 성능 함정은, 화면을 바꾸는 조작과 배치 결과를 읽는 조작을 뒤섞어 반복하는 것이다. 브라우저는 원래 여러 변경을 모아 두었다가 한 번에 배치를 다시 계산하려 하는데, 이 흐름을 깨는 습관이 문제를 일으킨다.
문제는 무언가를 바꾼 직후에 그 결과로 정해질 위치나 크기 같은 값을 곧바로 읽으려 할 때 생긴다. 브라우저는 정확한 값을 돌려주기 위해, 미뤄 두었던 배치 계산을 그 자리에서 즉시 강제로 수행할 수밖에 없다. 이렇게 계산이 앞당겨진다. 이런 조작을 반복 안에서 되풀이하면 상황이 심각해진다. 바꾸고 읽고, 다시 바꾸고 읽는 것을 여러 번 반복하면, 그때마다 브라우저가 배치를 강제로 다시 계산해 같은 일을 수십 번 되풀이한다.
이 함정을 푸는 방법은 읽는 일과 쓰는 일을 서로 분리하는 것이다. 필요한 값들을 먼저 모두 읽어 따로 담아 둔 뒤, 그다음에 변경을 한꺼번에 적용한다. 이렇게 순서를 나누면 브라우저가 배치를 한 번만 다시 계산하면 된다. 겉으로 보면 코드의 순서만 살짝 바꾼 것 같지만, 그 안에서 벌어지는 계산의 횟수는 수십 배로 달라질 수 있다.
예를 들어 여러 요소의 현재 높이를 참고해 각각의 크기를 조정해야 한다면, 먼저 모든 높이를 offsetHeight 같은 값으로 한꺼번에 읽어 배열에 담고, 그 뒤에 크기 변경을 몰아서 적용한다. 읽기와 쓰기가 교차하지 않으면 강제 재계산이 사라진다. 이 하나의 습관을 바로잡는 것만으로 버벅이던 화면이 눈에 띄게 부드러워지는 경우가 많다.
파장을 가두는 울타리
배치 계산의 비용이 연쇄성에서 온다면, 그 연쇄를 특정 범위 안에 가두는 것도 하나의 대책이 된다. 어떤 요소의 내부에서 일어난 변화가 그 바깥으로 퍼지지 않도록 경계를 세우면, 그 요소 안이 아무리 복잡하게 바뀌어도 재계산은 그 안에서 끝난다. 브라우저에 특정 요소가 독립적인 구획임을 알려 주면 이런 격리가 가능해진다.
이 격리가 유용한 곳은 서로 무관한 여러 조각이 한 화면에 모여 있는 경우다. 게시물 목록처럼 각 항목이 독립적으로 갱신되는 화면에서, 한 항목의 변화가 전체 목록의 자리를 다시 잡게 만들면 낭비가 크다. 각 항목을 독립 구획으로 선언해 두면, 하나가 바뀌어도 나머지 항목의 자리는 그대로 유지되어 재계산 범위가 극적으로 줄어든다.
비슷한 발상으로, 지금 화면에 보이지 않는 부분의 배치 계산을 아예 미뤄 두는 방법도 있다. 긴 문서에서 화면 밖으로 한참 벗어난 부분은 당장 자리를 정확히 잡을 필요가 없다. 브라우저에 그 부분이 화면 밖임을 알려 주면, 실제로 그 근처로 스크롤이 다가올 때까지 배치 계산을 아껴 둔다. 처음 화면을 세우는 데 드는 계산량이 이 방식으로 크게 줄어든다.
이런 격리와 지연은 모두 계산할 범위를 좁힌다는 하나의 원리에서 나온다. 화면 전체를 한 덩어리로 다루면 작은 변화도 전체를 흔들지만, 화면을 서로 독립적인 구획으로 나누어 두면 변화의 파장이 각 구획 안에 갇힌다. 구조를 설계할 때 어디에 이 울타리를 세울지를 미리 정해 두면, 나중에 화면이 복잡해져도 배치 계산이 감당할 수 없이 불어나는 일을 막을 수 있다.
다만 이 울타리도 무작정 두른다고 이득이 되는 것은 아니다. 격리를 선언하면 브라우저는 그 구획을 독립적으로 관리하기 위해 별도의 준비를 하는데, 이 준비에도 나름의 비용이 든다. 아주 작고 거의 바뀌지 않는 요소까지 일일이 격리하면, 얻는 이득보다 관리 비용이 커질 수 있다. 그래서 이 격리는 내부가 자주 바뀌면서도 바깥과 뚜렷이 독립적인 구획에 골라 적용하는 것이 옳다.
격리를 어디에 둘지 판단할 때는 그 구획의 변화가 정말로 바깥과 무관한지를 먼저 따져야 한다. 어떤 요소는 겉보기에 독립적이어도 그 크기가 바뀌면 옆 요소를 밀어내므로, 그런 요소를 격리하면 오히려 예상과 다른 결과가 나온다. 반대로 크기가 고정되어 있어 안이 바뀌어도 바깥 자리에는 영향을 주지 않는 구획이라면, 격리의 효과가 온전히 나타난다. 이 판단이 정확해야 울타리가 제 몫을 한다.
배치를 아끼는 실무의 지혜
배치 계산을 아끼는 첫 번째 지혜는, 앞서 말한 대로 화면 변경을 한데 모아 처리하는 것이다. 여러 곳을 바꿔야 한다면 그것들을 잠시 모아 두었다가 한 번에 반영해, 배치가 여러 번 다시 일어나지 않도록 한다. 이 모으기 습관이 부드러운 화면의 기본이다.
두 번째 지혜는 화면을 크게 흔드는 변경을 되도록 화면 바깥에서 준비하는 것이다. 여러 요소를 새로 만들어 붙일 때, 하나씩 화면에 붙이면 그때마다 배치가 다시 일어나지만, 미리 한데 묶어 두었다가 완성된 덩어리를 한 번에 붙이면 배치는 한 번으로 끝난다. 세 번째 지혜는 화면의 움직임을 브라우저의 그리기 리듬에 맞추는 것이다. 브라우저는 일정한 박자로 화면을 새로 그리는데, 변경을 이 박자에 맞춰 적용하면 불필요하게 박자를 벗어난 계산을 피할 수 있다.
네 번째 지혜는 배치에 영향을 주는 변경과 그렇지 않은 변경을 구분하는 것이다. 어떤 화면 변화는 자리를 다시 잡을 필요 없이 색만 바꾸면 되는데, 이런 변화를 골라 쓰면 무거운 배치 계산을 아예 건너뛸 수 있다. 이 구분은 다음 편의 색칠 이야기와 자연스럽게 이어진다. 다섯 번째 지혜는 근본적으로 구조를 가볍게 유지하는 것이다.
계산할 요소가 애초에 적으면 배치가 아무리 자주 일어나도 부담이 덜하다. 화면을 설계할 때부터 불필요한 겹싸기를 줄여, 나중에 성능으로 고생할 여지를 미리 없애야 한다. 성능 문제는 다 만든 뒤에 고치려 하면 훨씬 어렵기 때문이다. 정리하면, 브라우저는 그릴 것을 골라내고 그것들의 자리를 계산하는데, 이 배치 계산은 무겁고 자주 반복되므로 다루는 방식이 성능을 좌우한다. 변경을 모으고, 읽기와 쓰기를 나누고, 구조를 가볍게 하는 지혜가 핵심이다. 다음 편에서는 이렇게 자리가 정해진 요소들에 실제로 색을 입히고 여러 겹을 합쳐 화면을 완성하는 과정으로 넘어간다.