앞 편에서 브라우저가 화면에 색을 입히고 여러 층을 하나로 합쳐 최종 화면을 완성하는 과정을 살펴보았다. 그런데 이렇게 완성된 화면은 가만히 멈춰 있는 그림이 아니다. 버튼을 누르면 창이 열리고, 목록을 넘기면 새 내용이 채워지며, 입력에 따라 화면이 살아 움직인다. 이 모든 움직임의 뒤에는 브라우저 안에서 끊임없이 돌아가는 스크립트가 있다. 이번 편은 이 스크립트가 브라우저 안에서 어떻게 실행되는지를 다룬다.
스크립트 실행의 원리를 이해하는 것은, 화면이 왜 가끔 멈칫하고 왜 어떤 코드는 유독 느린지를 풀어내는 열쇠다. 스크립트는 화면을 그리는 흐름과 아주 밀접하게 얽혀 있어서, 그 관계를 모르면 좋은 코드를 짜고도 뜻밖의 버벅임에 발목을 잡히기 쉽다. 이번 편에서는 스크립트가 실행되기까지의 과정과, 그것이 화면과 어떤 자리를 나눠 쓰는지, 그리고 무거운 작업이 왜 화면을 멈추게 하는지를 짚는다.
코드가 실행되기까지
작성한 스크립트는 사람이 읽기 좋은 글자의 나열일 뿐, 그 자체로는 기계가 곧바로 실행할 수 없다. 브라우저 안에는 이 글자를 받아 실제 동작으로 바꿔 주는 전담 일꾼이 들어 있다. 이 일꾼이 스크립트를 읽고 해석해 실행하는 것이 이 이야기의 출발점이다.
이 일꾼은 먼저 스크립트를 읽어 그 구조를 파악한다. 어디까지가 하나의 문장이고 어떤 것이 어떤 것에 속하는지를 해석해, 다룰 수 있는 형태로 바꾼다. 이 해석 과정은 스크립트가 많고 복잡할수록 시간이 더 걸린다. 그래서 스크립트의 양 자체가 첫 실행을 늦추는 요인이 되기도 한다. 해석이 끝나면 일꾼은 스크립트를 곧바로 실행할 수 있는 중간 형태로 바꾼다. 사람이 쓴 글자를 기계가 한 걸음씩 따라갈 수 있는 지시의 나열로 옮기는 셈이다.
흥미로운 점은 이 일꾼이 자주 실행되는 부분을 알아채, 그것을 더 빠른 형태로 다시 다듬는다는 것이다. 처음에는 무난한 속도로 실행하다가, 어떤 코드가 반복해서 쓰이는 것을 보면 그 부분만 골라 더 빠르게 돌아가도록 최적화한다. 자주 밟는 길일수록 더 잘 닦아 두는 셈이다.
다만 이 최적화에는 전제가 있다. 일꾼은 그동안 지켜본 패턴이 앞으로도 이어질 것이라 가정하고 길을 닦는데, 만약 갑자기 다른 형태의 값이 들어오면 닦아 둔 길을 버리고 원래 방식으로 되돌아간다. 그래서 같은 자리에서 값의 모양이 자꾸 바뀌면 최적화가 헛돌아 오히려 느려질 수 있다. 자주 도는 코드일수록 다루는 값의 모양을 일정하게 유지하는 것이 유리하다. 스크립트는 내려받는 데도 시간이 들지만 받은 뒤에 해석하고 실행 형태로 바꾸는 데도 시간이 들므로, 첫 화면에 꼭 필요하지 않은 스크립트는 뒤로 미루는 것이 좋다.
한 줄기 흐름의 힘과 한계
브라우저에서 스크립트를 실행하는 이 일꾼에게는 중요한 특징이 하나 있다. 여러 일을 동시에 처리하지 않고, 한 번에 하나씩 차례로만 처리한다는 것이다. 마치 좁은 외길을 따라 일감이 한 줄로 늘어서서 하나씩 통과하는 것과 같다.
이 한 줄기 흐름은 장점이자 한계다. 장점은 단순함이다. 한 번에 하나만 처리하므로, 두 작업이 같은 값을 동시에 건드려 뒤엉키는 혼란이 애초에 생기지 않는다. 여러 일을 동시에 다룰 때 흔히 겪는 골치 아픈 문제들을 이 방식은 원천적으로 피한다. 한계는 명확하다. 하나의 일감이 이 외길을 오래 붙들고 있으면, 그 뒤에 줄 선 다른 일감들은 모두 기다려야 한다.
여기서 꼭 알아야 할 사실은, 화면을 새로 그리는 일도 이 같은 외길을 지나간다는 점이다. 스크립트 실행과 화면 그리기가 같은 흐름을 나눠 쓰기 때문에, 스크립트가 길을 오래 막으면 그동안 화면을 새로 그릴 기회조차 오지 않는다. 그래서 무거운 스크립트가 도는 동안에는 화면이 얼어붙은 것처럼 멈춰 보인다.
사용자의 조작에 반응하는 일도 마찬가지다. 버튼을 눌렀을 때 그에 응답하는 처리 역시 이 외길을 지나야 하는데, 앞에 무거운 작업이 버티고 있으면 눌러도 한참 뒤에야 반응한다. 사용자가 느끼는 답답함의 상당수가 바로 이 외길이 막혀 있기 때문에 생긴다. 화면 그리기, 사용자 반응, 스크립트 실행이 모두 하나의 좁은 길을 나눠 쓴다는 그림을 머릿속에 그려 두면, 어떤 코드를 짤 때 이것이 그 길을 얼마나 오래 막을지를 자연히 의식하게 된다.
긴 작업이 화면을 멈추는 이유
무거운 스크립트가 화면을 멈추게 하는 원리를 조금 더 구체적으로 들여다본다. 브라우저는 일정한 박자로 화면을 새로 그리려 하는데, 이 박자 사이사이에 스크립트를 실행할 짬을 낸다. 스크립트가 이 짬 안에 끝나면 화면 그리기가 제때 이어져 매끄럽게 보인다.
문제는 하나의 스크립트 작업이 이 짬을 훌쩍 넘겨 오래 걸릴 때다. 그러면 다음 화면을 그릴 박자가 왔는데도 외길이 아직 그 작업에 붙들려 있어, 화면 그리기가 건너뛰어진다. 이렇게 박자를 한두 번 놓치면 사용자 눈에는 화면이 뚝뚝 끊기는 것처럼 보인다. 이렇게 외길을 오래 붙드는 작업을 긴 작업이라 부른다. 한 번에 아주 많은 데이터를 처리하거나, 복잡한 계산을 통째로 돌리거나, 방대한 목록을 한꺼번에 다루는 일이 대표적이다.
긴 작업의 무서운 점은, 코드를 짤 때는 그것이 얼마나 오래 걸릴지 잘 드러나지 않는다는 것이다. 적은 데이터로 시험할 때는 순식간에 끝나던 작업이, 실제 사용자의 방대한 데이터 앞에서는 외길을 오래 막는 긴 작업으로 돌변하곤 한다. 그래서 처리량이 커질 수 있는 작업은 늘 경계해야 한다.
긴 작업을 다루는 기본 원리는 그것을 잘게 쪼개는 것이다. 한 번에 다 처리하려 들지 말고, 조금 처리하고 잠시 외길을 비켜 주어 화면 그리기와 사용자 반응이 끼어들 틈을 준 뒤, 다시 이어서 처리하는 식이다. 전체 걸리는 시간은 비슷하더라도, 중간중간 길을 비켜 주므로 화면은 멈추지 않는다. 한 사람이 좁은 문을 오래 막지 않도록 짐을 나눠 지고 여러 번 오가는 것과 같다.
무거운 일을 옆으로 넘기기
긴 작업을 잘게 쪼개는 것만으로 부족할 때가 있다. 계산 자체가 워낙 무거워, 아무리 나눠도 외길을 자주 붙들 수밖에 없는 경우다. 이럴 때는 아예 그 작업을 외길 바깥의 별도 일꾼에게 넘기는 방법이 있다.
브라우저는 화면과 무관한 무거운 계산을 처리할 수 있는 별도의 일꾼을 마련해 두었다. 이 별도 일꾼은 화면을 그리는 외길과 떨어진 자리에서 돌아가므로, 아무리 무거운 계산을 돌려도 화면이 멈추지 않는다. 무거운 짐을 아예 다른 방으로 옮겨 처리하는 셈이다. 이 방식의 핵심은 두 일꾼이 서로 자리를 침범하지 않는다는 것이다.
별도 일꾼은 화면을 직접 만지지 못하는 대신 계산에만 몰두한다. 화면을 담당하는 본래 일꾼은 그 사이에도 조작에 반응하고 화면을 그릴 수 있다. 계산이 끝나면 별도 일꾼이 결과만 건네주고, 그 결과를 화면에 반영하는 일은 본래 일꾼이 맡는다. 이렇게 일을 나누면 방대한 데이터를 정리하거나 복잡한 분석을 돌리는 무거운 작업을 화면 뒤편에서 조용히 처리하면서도 화면은 계속 살아 움직이게 할 수 있다.
다만 이 방식에도 대가가 있다. 두 일꾼이 서로 결과를 주고받으려면 값을 건네는 절차가 필요한데, 주고받는 데이터가 아주 크면 그 전달 자체에 시간이 든다. 그래서 잦게 조금씩 주고받기보다, 큰 계산을 한 번 넘기고 결과를 한 번 받아 오는 식으로 전달 횟수를 아끼는 것이 좋다. 대부분의 버벅임은 긴 작업을 잘게 쪼개는 것만으로 풀리지만, 그것으로 감당이 안 되는 진짜 무거운 계산에는 이 방법이 확실한 해답이 된다.
쓰지 않는 값을 정리하는 일
스크립트가 돌아가는 동안 크고 작은 값들이 끊임없이 만들어진다. 이 값들이 차지한 자리를 언젠가는 되돌려주어야 하는데, 그 일을 사람이 일일이 챙기는 것은 번거롭고 실수가 잦다. 그래서 브라우저 안의 일꾼은 더 이상 쓰이지 않는 값을 스스로 알아채 그 자리를 회수하는 정리 작업을 주기적으로 수행한다.
이 정리는 어떤 값이 아직 어딘가에서 참조되고 있는지를 따져 이뤄진다. 어디에서도 더 이상 가리키지 않는 값은 앞으로 쓰일 수 없으므로 안전하게 치울 수 있다. 반대로 한 군데라도 그 값을 붙들고 있으면 치우지 못한다. 그래서 다 쓴 값인데도 어딘가에 참조가 남아 있으면, 그 값은 회수되지 못하고 자리를 계속 차지한다. 이렇게 쓰지 않는 값이 치워지지 못하고 쌓이는 것을 누수라 부른다.
누수가 무서운 이유는 조용히 진행되기 때문이다. 페이지를 오래 열어 둘수록 회수되지 못한 값이 야금야금 쌓여, 처음에는 멀쩡하던 화면이 시간이 지날수록 무거워지고 결국 버벅이거나 멈춘다. 화면을 오래 켜 두는 종류의 서비스에서 이 문제가 특히 두드러진다. 흔한 원인은 이미 화면에서 사라진 요소를 어떤 목록이나 처리기가 여전히 붙들고 있는 경우다.
정리 작업 자체도 공짜가 아니라는 점도 알아 두어야 한다. 일꾼이 값을 회수하는 동안에는 잠깐 다른 일을 멈추므로, 회수할 값이 많으면 그 순간 화면이 살짝 끊길 수 있다. 그래서 짧은 순간에 값을 대량으로 만들었다 버리는 코드는 회수 부담을 키운다. 꼭 필요한 값만 만들고, 다 쓴 참조는 분명히 놓아주며, 반복 안에서 값을 무분별하게 찍어 내지 않는 습관이 정리 부담과 누수를 함께 줄인다.
누수를 찾아내는 방법은 시간을 두고 자리 차지의 변화를 지켜보는 것이다. 개발자 도구로 어느 순간의 값 사용량을 재어 두고, 같은 조작을 여러 번 반복한 뒤 다시 재면, 회수되어야 할 자리가 계속 늘고 있는지를 알 수 있다. 조작을 반복할 때마다 사용량이 한 단계씩 올라가고 내려오지 않는다면, 그 조작 어딘가에서 참조가 남아 값이 치워지지 못하는 것이다. 이렇게 늘어나는 지점을 좁혀 가면 누수의 근원을 짚을 수 있다.
실행을 가볍게 유지하는 습관
스크립트 실행을 가볍게 유지하는 첫 번째 습관은, 첫 화면에 필요하지 않은 스크립트를 뒤로 미루는 것이다. 모든 스크립트를 처음부터 다 싣고 해석하면 첫 화면이 그만큼 늦어진다. 당장 필요한 것만 먼저 싣고, 나머지는 실제로 쓰일 때 불러오는 식으로 첫 부담을 줄여야 한다.
두 번째 습관은 앞서 강조한 대로 긴 작업을 만들지 않는 것이다. 한 번에 많은 것을 처리하려는 코드를 발견하면, 그것을 잘게 나눌 수 있는지부터 살핀다. 특히 사용자의 조작에 곧바로 반응해야 하는 자리에서는, 무거운 계산을 그 자리에서 통째로 돌리지 않도록 각별히 조심해야 한다. 세 번째 습관은 같은 일을 쓸데없이 반복하지 않는 것이다.
이미 계산해 둔 결과를 매번 처음부터 다시 구하거나, 바뀌지 않은 값을 거듭 다시 만드는 코드는 외길을 낭비한다. 한 번 구한 결과는 잘 갈무리해 두었다가 다시 쓰는 식으로 불필요한 반복을 걷어내야 한다. 네 번째 습관은 자주 도는 코드가 다루는 값의 모양을 일정하게 유지하는 것이다. 앞서 말했듯 실행 일꾼은 일정한 패턴을 전제로 코드를 빠르게 다듬는데, 값의 모양이 자꾸 바뀌면 그 최적화가 무너진다.
다섯 번째 습관은 정말 무거운 계산은 아낌없이 별도 일꾼에게 넘기는 것이다. 화면을 지켜야 하는 상황에서 감당하기 힘든 계산이 있다면, 그것을 외길 바깥으로 옮기는 결단을 미루지 않는다. 정리하면, 스크립트는 브라우저 안의 전담 일꾼이 해석하고 실행 형태로 바꾸어 한 줄기 외길에서 하나씩 처리하며, 이 외길을 화면 그리기와 사용자 반응이 함께 나눠 쓴다. 그래서 긴 작업이 길을 막지 않게 하는 것이 부드러운 화면의 핵심이다. 다음 편에서는 이 외길 위에서 일감들이 어떤 순서로 처리되는지를 결정하는 순환의 원리를 살펴본다.