앞 편에서 스크립트가 브라우저 안의 한 줄기 외길에서 하나씩 처리되며, 이 외길을 화면 그리기와 사용자 반응이 함께 나눠 쓴다는 것을 살펴보았다. 그렇다면 이 외길 위로 여러 일감이 몰려들 때, 무엇을 먼저 처리하고 무엇을 나중으로 미룰지는 누가 정하는가. 이번 편은 이 순서를 결정하며 외길을 쉬지 않고 돌리는 순환의 원리를 다룬다.


이 순환의 원리는 스크립트를 다루는 사람이라면 반드시 몸에 익혀야 하는 기초다. 이것을 모르면 코드가 왜 이런 순서로 실행되는지, 왜 어떤 일은 곧바로 처리되고 어떤 일은 한 박자 늦게 처리되는지가 도무지 예측이 안 된다. 반대로 이 순환의 리듬을 이해하면, 겉보기에 뒤죽박죽인 실행 순서가 사실은 일정한 규칙을 따른다는 것을 알게 된다.

쉬지 않고 도는 순환

브라우저 안에는 외길 위의 일감을 관리하는 순환 장치가 있다. 이 장치는 아주 단순한 일을 끝없이 되풀이한다. 처리할 일감이 있는지 살피고, 있으면 하나 꺼내 외길로 보내 처리하고, 끝나면 다시 다음 일감이 있는지 살피는 것이다. 이 살피고 처리하기를 쉬지 않고 반복한다.


이 순환이 도는 덕분에 화면은 살아 있는 것처럼 반응한다. 사용자가 무언가를 조작하면 그에 해당하는 일감이 만들어져 줄을 서고, 순환 장치가 그것을 꺼내 처리한다. 시간이 지나 실행돼야 할 예약된 일감도, 바깥에서 무언가가 도착해 생긴 일감도 모두 이 순환을 통해 차례로 다뤄진다.


중요한 점은 이 순환이 한 번에 하나의 일감만 완전히 처리한다는 것이다. 하나를 꺼내 처리하기 시작하면, 그것이 끝날 때까지 다른 일감을 건드리지 않는다. 앞 편에서 말한 외길의 성질이 바로 여기서 비롯된다. 순환 장치는 일감을 동시에 여럿 처리하는 것이 아니라, 철저히 한 줄로 세워 하나씩 통과시킨다. 그래서 하나의 일감이 오래 걸리면 순환 전체가 그동안 멈춰 있다.


순환 장치는 또한 매 순환 사이에 화면을 새로 그릴 기회를 살핀다. 일감 하나를 처리하고 나면, 화면을 다시 그릴 때가 되었는지를 확인하고 필요하면 그린다. 그래서 일감들이 짧게 짧게 끝나면 그 사이사이 화면 그리기가 촘촘히 끼어들어 매끄럽게 보인다. 반대로 일감이 길면 이 그리기 기회가 뒤로 밀린다. 이 순환 장치는 절대 두 일감을 동시에 처리하지 않고, 하나가 완전히 끝난 뒤에야 다음을 부르며, 일감 사이에 잠깐 짬을 내어 화면을 정돈한다.

큰 일감과 작은 일감

순환 장치가 다루는 일감에는 사실 두 종류가 있다. 이것을 큰 일감과 작은 일감이라 부르겠다. 이 둘은 줄을 서는 자리가 다르고, 처리되는 시점도 다르다. 이 구분이 실행 순서를 이해하는 핵심 열쇠다.


큰 일감은 순환의 한 바퀴마다 하나씩 처리되는 굵직한 일감이다. 사용자의 조작에 대한 반응이나, 시간이 지나 실행되도록 예약된 일, 바깥에서 무언가 도착해 생긴 일 등이 여기에 속한다. 순환 장치는 한 바퀴에 이런 큰 일감을 하나 꺼내 처리하고 다음 바퀴로 넘어간다. 작은 일감은 성격이 다르다. 이것은 방금 처리한 큰 일감에 딸려 곧바로 처리돼야 하는 자잘한 뒤처리 같은 것이다.


어떤 일이 끝난 직후에 이어서 해야 하는 마무리 작업들이 작은 일감에 담긴다. 작은 일감은 큰 일감과 달리 별도의 줄에 모인다. 두 종류의 결정적인 차이는 처리되는 시점이다. 순환 장치는 큰 일감 하나를 처리한 직후, 다음 큰 일감으로 넘어가기 전에 작은 일감 줄을 통째로 비운다. 쌓여 있는 작은 일감을 하나도 남김없이 다 처리한 뒤에야 비로소 다음 큰 일감으로 넘어가는 것이다.


이 규칙 때문에 작은 일감은 큰 일감보다 항상 먼저 처리된다. 아무리 나중에 만들어졌더라도, 작은 일감으로 등록되면 다음 큰 일감이 시작되기 전에 반드시 처리된다. 작은 일감은 지금 이 순간의 뒤처리이고 큰 일감은 다음 차례의 일이라고 새기면 헷갈리지 않는다. 이 두 줄의 구분을 이해하면, 왜 어떤 코드가 먼저 예약했는데도 늦게 실행되고 나중에 예약한 것이 먼저 실행되는지를 납득할 수 있다. 그것은 순서가 뒤바뀐 것이 아니라, 애초에 서로 다른 줄에 선 것이다.

작은 일감이 먼저 처리되는 까닭

왜 브라우저는 작은 일감을 큰 일감보다 먼저, 그것도 몰아서 처리하도록 만들었는가. 여기에는 그럴 만한 이유가 있다. 작은 일감은 대개 방금 끝난 일의 마무리이므로, 다른 일이 끼어들기 전에 깔끔히 매듭짓는 편이 상태를 일관되게 지키는 데 유리하기 때문이다.


만약 작은 일감을 큰 일감들 사이사이에 띄엄띄엄 처리한다면, 마무리가 끝나기 전에 다른 큰 일이 끼어들어 어중간한 상태에서 일이 뒤섞일 수 있다. 마무리는 마무리끼리 몰아서 한 번에 끝내는 편이, 어중간한 중간 상태가 바깥에 드러나는 것을 막아 준다. 덕분에 어떤 일이 끝난 직후에 반드시 이어져야 하는 뒤처리를 작은 일감으로 맡길 수 있다. 그러면 그 뒤처리는 다음 큰 일이 시작되기 전에 확실히 끝나 있으리라 믿을 수 있다.


다만 이 몰아서 처리하기에는 조심할 점이 있다. 작은 일감을 처리하는 도중에 또 새로운 작은 일감이 만들어지면, 그것 역시 지금 이 줄이 다 비워지기 전에 처리된다. 즉 작은 일감이 꼬리에 꼬리를 물고 계속 생겨나면, 순환 장치는 그 줄을 비우느라 다음 큰 일감으로 넘어가지 못한다.


이렇게 작은 일감이 끝없이 이어지면 심각한 문제가 생긴다. 다음 큰 일감도, 그 사이에 있어야 할 화면 그리기도 영영 차례가 오지 않는다. 화면이 완전히 멈추고 조작에도 반응하지 않는 상태가 된다. 그래서 무언가를 계속 반복해야 할 때, 그것을 작은 일감으로 이어 붙이기보다 큰 일감으로 다루어 순환에 숨 쉴 틈을 주어야 한다. 큰 일감은 한 바퀴에 하나씩 처리되고 그 사이에 화면 그리기가 끼어들 수 있으므로, 반복 중에도 화면이 멈추지 않는다.

순환을 이해하면 순서가 보인다

순환의 규칙을 알면 겉보기에 복잡한 실행 순서도 차분히 예측할 수 있다. 어떤 코드가 있을 때, 그 안의 각 부분이 지금 당장 처리될 것인지, 작은 일감 줄에 들어갈 것인지, 큰 일감 줄에 들어갈 것인지를 나누어 보면 순서가 드러난다.


먼저 지금 실행 중인 코드의 흐름은 중간에 끊기지 않고 끝까지 이어진다. 순환 장치는 지금 처리 중인 일감을 완전히 끝내야 다음으로 넘어가므로, 실행 도중에 다른 일이 불쑥 끼어들지 않는다. 그래서 지금 흐름 안에 곧바로 적힌 일들은 모두 순서대로 먼저 처리된다. 그다음, 지금 흐름이 끝나면 순환 장치는 쌓인 작은 일감 줄을 비운다.


실행 도중에 작은 일감으로 등록된 마무리 작업들이 이때 한꺼번에 처리된다. 이것들은 다음 큰 일감보다 먼저 처리되므로, 지금 흐름 바로 다음에 이어진다고 보면 된다. 마지막으로, 작은 일감 줄까지 다 비운 뒤에야 순환 장치는 다음 큰 일감으로 넘어간다. 예약된 일이나 다음 조작에 대한 반응처럼 큰 일감으로 등록된 것들은 이 마지막 차례에 처리된다. 그래서 아무리 짧은 시간 뒤로 예약했더라도, 지금 흐름과 작은 일감보다는 늦게 처리된다.


이 세 단계를 순서대로 짚으면, 여러 종류의 일이 뒤섞인 코드에서도 무엇이 먼저 나올지를 또렷이 그릴 수 있다. 실행 순서가 헷갈리는 코드를 만나면, 각 부분이 이 세 자리 중 어디에 속하는지를 하나하나 표시해 보면 된다. 그러면 뒤죽박죽처럼 보이던 순서가 질서 있게 정리된다.


이 예측 능력은 실전에서 생각보다 자주 쓰인다. 어떤 값이 갱신되는 시점을 다른 처리와 맞춰야 하거나, 여러 비동기 작업의 앞뒤를 정확히 가늠해야 할 때, 이 순환의 규칙이 든든한 나침반이 된다. 순서를 짐작이 아니라 규칙으로 아는 것과 모르는 것의 차이는 크다.

예약된 시간의 부정확함

순환의 규칙을 알면 시간을 정해 일을 예약할 때 흔히 겪는 오해도 풀린다. 어떤 일을 일정한 시간 뒤에 실행하도록 예약하면, 사람들은 정확히 그 시간에 일이 실행되리라 기대한다. 그러나 예약된 시간은 실행을 보장하는 약속이 아니라, 그 시간이 지나기 전에는 실행하지 않겠다는 최소한의 지연일 뿐이다.


예약된 일은 큰 일감으로 순환에 올라선다. 따라서 예약한 시간이 되었더라도 순환 장치가 지금 다른 일감에 붙들려 있으면, 그 일이 끝나고 작은 일감 줄까지 다 비운 뒤에야 차례가 온다. 앞의 일감이 오래 걸리면 예약된 시간이 한참 지나서야 실행되는 것이다. 그래서 정밀한 시간 간격이 필요한 처리를 예약에만 의존하면, 실제 간격이 들쭉날쭉해진다.


이 부정확함은 짧은 간격으로 반복을 예약할 때 특히 문제가 된다. 한 번의 실행이 예약한 간격보다 오래 걸리면, 다음 실행이 밀리고 그렇게 밀린 것들이 서로 겹쳐 쌓인다. 순환은 밀린 일감을 처리하느라 점점 뒤처지고, 결국 화면 그리기가 끼어들 틈마저 잃는다. 반복을 예약할 때는 다음 예약을 이번 실행이 끝난 뒤에 거는 방식으로 겹침을 막는 편이 안전하다.


화면의 움직임을 다룰 때는 시간을 직접 예약하기보다 화면 그리기의 박자에 맞추는 편이 낫다. 순환 장치가 화면을 다시 그릴 때에 맞춰 변화를 적용하면, 그리기와 어긋나 헛도는 계산이 사라지고 움직임이 화면의 리듬과 정확히 포개진다. 시간의 예약은 대략의 지연에는 쓸모가 있지만, 매 순간의 정밀한 움직임에는 그리기 박자에 올라타는 편이 순환의 성질과 더 잘 맞는다.


정밀한 시간 간격이 꼭 필요한 처리라면, 예약된 시점을 그대로 믿기보다 실제로 흐른 시간을 매번 확인해 보정하는 편이 낫다. 예약이 조금 늦게 실행되었더라도 그 사이 실제로 얼마가 흘렀는지를 재어, 그만큼을 반영해 다음을 계산하면 누적된 오차가 커지지 않는다. 순환의 부정확함을 없앨 수는 없으니, 그 부정확함을 매 걸음 재어 바로잡는 것이 정밀함을 지키는 현실적인 방법이다.

순환을 막지 않는 법

건강한 순환을 지키는 첫 번째 원칙은, 하나의 일감이 순환을 오래 붙들지 않게 하는 것이다. 앞 편에서 다룬 긴 작업 이야기가 여기서 그대로 이어진다. 무거운 일은 잘게 쪼개 여러 큰 일감으로 나누어, 그 사이사이 순환이 돌며 화면을 그리고 조작에 반응할 틈을 준다.


두 번째 원칙은 작은 일감을 무한히 이어 붙이지 않는 것이다. 마무리 작업 안에서 또 마무리 작업을 끝없이 만들어 내면, 순환 장치가 그 줄을 비우느라 다음으로 넘어가지 못해 화면이 멈춘다. 되풀이가 필요한 일은 작은 일감이 아니라 큰 일감으로 다루어 순환에 숨통을 틔워야 한다. 세 번째 원칙은 화면 그리기의 박자에 맞춰 화면 변화를 적용하는 것이다.


순환 장치는 매 순환 사이에 화면을 그릴 기회를 살피는데, 화면을 바꾸는 일을 이 박자에 맞추면 불필요하게 어긋난 갱신을 피할 수 있다. 네 번째 원칙은 사용자의 조작에 반응하는 자리에서 무거운 일을 곧바로 다 처리하려 하지 않는 것이다. 조작에 대한 반응은 큰 일감으로 순환에 올라타는데, 그 안에서 무거운 계산을 통째로 돌리면 반응이 끝날 때까지 순환이 멈춘다. 반응 자리에서는 당장 필요한 최소한만 처리하고, 무거운 뒤처리는 다음 순환으로 미뤄야 한다.


다섯 번째 원칙은 순환의 상태를 눈으로 확인하며 다루는 것이다. 어떤 일감이 순환을 얼마나 오래 붙들고 있는지는 짐작만으로는 알기 어렵다. 순환의 흐름과 각 일감의 길이를 들여다볼 수 있는 도구로 실제 상황을 확인하고 나서 판단해야 한다. 이 확인 습관은 이 서브영역의 마지막 편에서 따로 다룬다. 정리하면, 순환 장치는 외길 위의 일감을 한 바퀴에 하나씩 꺼내 처리하되, 큰 일감 사이사이에 작은 일감 줄을 통째로 비우고 화면을 그릴 기회를 살핀다. 이 규칙을 알면 실행 순서를 예측할 수 있고, 순환을 막지 않는 코드를 짤 수 있다. 다음 편에서는 이 순환 위에서 벌어지는 화면 변화 가운데, 자리를 다시 잡게 만드는 무거운 변화와 색만 다시 칠하면 되는 가벼운 변화를 더 깊이 구분해 본다.