처리중입니다. 잠시만 기다려주세요.
TTJ 코딩클래스
정규반 단과 자료실 테크 뉴스 코딩 퀴즈
테크 뉴스
Hacker News 2026.09.03 35

브라우저 메인 스레드는 비싼 자원이다: 화면 멈춤을 다루는 법

Hacker News 원문 보기

프론트엔드 최적화라고 하면 대개 네트워크 요청 줄이기, 번들 크기 축소, 캐시 활용 같은 것을 떠올린다. 여기에 리렌더링 감소나 리소스 로딩 시점 조정 정도가 더해진다. 그런데 메인 스레드는 이 목록에 잘 등장하지 않는다. 이유는 단순하다. 대부분의 화면에서는 메인 스레드가 문제가 되지 않기 때문이다. 하지만 데이터가 실시간으로 흘러 들어오고 스크롤, 애니메이션, 입력이 한데 얽히는 인터랙션 많은 화면에서는 이야기가 달라진다. 네트워크와 번들을 아무리 아껴도, 메인 스레드가 막히는 순간 화면은 그 자리에서 얼어붙는다.

스크롤이 이따금 끊기거나, 버튼 반응이 살짝 늦거나, 검색창에 입력한 글자가 반 박자 늦게 뜨는 사이트를 경험해 봤을 것이다. 짜증날 정도는 아니지만 은근히 신경을 긁는 그 미묘한 버벅임, 이른바 '잰크(jank)'가 바로 막힌 메인 스레드의 모습이다. 개발자는 이런 현상을 만나면 흔히 '내 코드가 느린가?' 하며 알고리즘을 뜯어보거나 낭비된 연산을 찾는다. 그러나 대개 문제는 코드의 속도가 아니다. 코드가 느린 게 아니라, 그 코드가 하필 메인 스레드를 붙들고 있는 코드인 것이다.

하나의 스레드에 몰린 두 가지 일

브라우저에는 여러 스레드가 있지만, 우리가 코드로 만질 수 있는 거의 모든 것은 메인 스레드에 집중되어 있다. 연산, 렌더링, 이벤트 처리, 네트워크 응답 처리, 그리고 프레임워크 내부 동작까지 모두 이곳에서 처리된다. 자원은 하나인데 일은 산더미다. 메인 스레드가 하는 일은 크게 두 갈래로 나뉜다. 하나는 자바스크립트 실행이다. 우리가 작성한 코드, 이벤트 핸들러, 타이머, 네트워크 콜백, 프레임워크 내부가 모두 여기서 돈다. 다른 하나는 화면 그리기다. DOM이나 스타일이 바뀌어 갱신이 필요할 때 브라우저는 정해진 단계를 거쳐 한 프레임을 만들어 내며, 이 파이프라인의 앞부분은 대부분 메인 스레드의 몫이다. 마지막 합성 단계만 컴포지터 스레드로 넘어간다.

문제는 이 두 종류의 일이 같은 스레드 위에 한 줄로 늘어선다는 점이다. 자바스크립트는 단일 스레드 이벤트 루프 모델로 설계되어, 메인 스레드는 한 번에 하나의 작업만 처리하고 그 작업이 도는 동안에는 아무 일도 일어나지 못한다. 60Hz 디스플레이에서 화면이 매끄러우려면 프레임당 약 16.6밀리초 안에 그림을 완성해야 하는데, 브라우저 자체 비용을 빼면 실질 예산은 대략 10밀리초 정도로 본다. 만약 함수 하나가 200밀리초를 잡아먹으면 그동안 브라우저는 화면을 다시 그리지도, 클릭을 받지도 못한다. 이렇게 오래 메인 스레드를 붙드는 작업을 롱 태스크라 부르며, 보통 50밀리초를 넘으면 문제로 간주한다. 이 이야기는 사용자의 상호작용 응답 시간을 재는 INP, 로딩 중 메인 스레드가 막힌 총 시간을 재는 TBT 같은 성능 지표와 그대로 이어진다. 결국 성능 최적화의 상당 부분은 이 한 개의 스레드를 얼마나 세심하게 쓰느냐의 문제다.

안에서 시간을 나눠 쓰기: 쪼개기의 힘

메인 스레드를 아껴 쓰는 길은 크게 두 갈래다. 하나는 스레드 안에서 시간을 잘 나눠 쓰는 것이고, 다른 하나는 아예 일을 스레드 밖으로 내보내는 것이다. 첫 번째 갈래에는 쪼개기, 묶기, 우선순위 매기기, 미루기라는 네 가지 수가 있는데, 이 중 쪼개기가 나머지의 토대다. 작업에 경계가 있어야 그 사이에 무엇을 끼우고 무엇을 뒤로 밀지 정할 수 있기 때문이다. 인기 라이브 스트리밍의 채팅창을 떠올려 보자. 초당 수백 개씩 몰려드는 메시지를 도착하는 즉시 통째로 렌더링하면, 각 메시지마다 DOM 생성·스타일 계산·레이아웃·페인트가 따라붙어 수백 번의 반복이 한 작업 안에서 연달아 돈다. 그동안 내가 입력하려는 글자는 버벅이고, 다른 애니메이션도 함께 끊긴다. 남의 채팅이 메인 스레드를 독점해 내 일을 방해하는 셈이다.

해법은 그 덩어리를 작은 조각으로 자르고, 조각 사이에서 잠깐 메인 스레드의 제어권을 돌려주는 것이다. 그 틈에 브라우저는 밀려 있던 화면 갱신과 입력 처리를 따라잡는다. 여기서 오해하지 말아야 할 점은, 양보(yield)가 일을 더 빠르게 만들지는 않는다는 것이다. 전체 작업량은 그대로이고 양보할 때마다 기다리는 시간이 추가되므로 실제 소요 시간은 오히려 늘어난다. 그런데도 입력과 렌더링이 되살아나는 이유는, 프레임을 만드는 렌더링 파이프라인이 작업 도중에는 끼어들지 못하고 오직 작업과 작업 사이에서만 돌 수 있기 때문이다. 양보는 바로 그 틈을 만드는 행위다. 코드 수준에서 가장 고전적인 방법은 setTimeout으로 남은 작업을 다음 태스크로 넘기는 것이고, 애니메이션이 돌거나 스크롤 중이라면 개수 대신 시간으로 쪼개는 편이 안전하다. 이때는 performance.now()로 예산 초과 여부를 재고, requestAnimationFrame으로 다음 프레임 직전에 재개하도록 맞춘다. 프레임 시작 시각을 기준점으로 삼으면 같은 프레임을 여러 애니메이션이 나눠 쓸 때도 예산이 자연스럽게 지켜진다. 흔히 쓰는 5밀리초라는 값에 특별한 근거가 있는 것은 아니고, 10밀리초 남짓의 실질 예산 중 절반쯤을 배경 작업에 주고 나머지를 애니메이션과 스타일·레이아웃·페인트에 남기는 어림잡은 기준일 뿐이다.

도구마다 다른 성격, 그리고 한계

쪼개기를 적용할 때 유의할 점도 있다. 너무 잘게 나누면 양보하고 돌아오는 비용이 정작 하려던 일보다 커져 역효과가 난다. 또 setTimeout으로 양보하면 최소 지연이 있어 각 조각이 몇 밀리초씩 헛되이 기다릴 수 있는데, 아주 높은 응답성이 필요한 상황에서는 이 지연이 걸림돌이 된다. 그래서 어떤 코드는 MessageChannel로 메시지를 보내 다음 태스크를 예약하며, 리액트의 스케줄러가 이 방식을 쓴다. 최근에는 이 문제를 겨냥한 표준 API인 scheduler.yield()도 등장했는데, 양보 후 원래 작업이 대기열 뒤로 밀리지 않고 다른 태스크보다 먼저 재개된다는 장점이 있지만 브라우저 지원은 아직 고르지 않다. 도구마다 돌아오는 시점이 다르다는 점도 기억할 만하다. setTimeout과 scheduler.yield()는 렌더링 주기와 무관하게 재개되는 반면 requestAnimationFrame은 프레임이 그려지기 직전에 재개되므로, 화면 갱신과 박자를 맞춰야 하는 작업에는 rAF가 더 적합하다.

결국 이 글이 짚는 요점은, 메인 스레드가 비싼 단일 자원이며 대부분의 상황에서는 문제가 되지 않지만 야심 찬 화면을 만들려는 순간부터 그 자원을 다루는 일이 핵심이 된다는 것이다. 코드가 느려서가 아니라 메인 스레드를 오래 붙들기 때문에 화면이 멈춘다는 관점의 전환은, 실무에서 잰크의 원인을 엉뚱한 곳에서 찾느라 시간을 낭비하지 않게 해 준다. 다만 쪼개기를 비롯한 기법들은 어디까지나 메인 스레드 안에서 시간을 재배분하는 수단일 뿐, 총 작업량 자체를 줄이지는 못한다는 한계를 함께 기억해야 한다. 진짜 무거운 연산이라면 웹 워커처럼 일을 스레드 밖으로 내보내는 다음 갈래까지 시야에 넣어야 하며, 어떤 도구로 언제 양보할지는 응답성과 프레임 동기화 중 무엇이 더 중요한 화면인지에 따라 신중히 골라야 한다.

이 뉴스가 유용했나요?

TTJ 코딩클래스 정규반

월급 외 수입,
코딩으로 만들 수 있습니다

17가지 수익 모델을 직접 실습하고, 1,300만원 상당의 자동화 도구와 소스코드를 받아가세요.

144+실전 강의
17개수익 모델
4.9수강생 평점
정규반 자세히 보기

"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"

실제 수강생 후기
  • 비전공자도 6개월이면 첫 수익
  • 20년 경력 개발자 직강
  • 자동화 프로그램 + 소스코드 제공

매일 AI·개발 뉴스를 받아보세요

주요 테크 뉴스를 매일 아침 이메일로 전해드립니다.

스팸 없이, 언제든 구독 취소 가능합니다.