사전 페인트
개념

페인트

gabury1고친 사람 github-actions[bot]

페인트는 브라우저가 화면에 무엇을 어떤 순서로 칠할지 정합니다. 웹 페이지의 요소마다 위치와 크기가 정해진 다음에 실행됩니다. 여기서 나온 그리기 명령이 픽셀로 바뀌어 화면에 뜹니다. 창을 띄우는 다른 프로그램도 제 모습을 그리는 일을 같은 이름으로 부릅니다.

쉽고 빠른 이해

페인트는 「이 위치에 파란 사각형, 그 위에 흰 글자」 같은 그리기 명령을 차례대로 적는 일입니다. 버튼 하나라면 배경을 먼저 칠하고 테두리를 두른 뒤 글자를 맨 위에 올리라고 적습니다.

위치와 크기만 정해서는 화면에 아무것도 안 나옵니다. 요소끼리 겹치면 누가 위에 보일지도 정해야 합니다. 이 둘을 정하는 단계가 페인트입니다.

  1. 위치와 크기 계산이 끝난 요소들을 받습니다
  2. 뒤에 깔릴 것부터 앞에 보일 것 순서로 그리기 명령을 적습니다
  3. 다음 단계가 그 명령을 픽셀로 바꿔 화면에 냅니다

대가는 색 하나만 바뀌어도 다시 칠해야 한다는 것입니다. 칠할 넓이가 넓거나 그림자처럼 칠하는 데 오래 걸리는 효과가 많으면 화면이 끊겨 보입니다.

상세

풍경화를 그리는 화가를 떠올려 봅시다. 화가는 하늘을 먼저 칠하고 그 위에 산을 칠합니다. 앞에 선 나무는 맨 나중에 칠합니다. 먼 것부터 칠해야 가까운 것이 먼 것을 덮어 가립니다.

브라우저의 페인트도 이 순서를 따릅니다. 페이지의 배경을 먼저 적습니다. 그 위에 놓인 상자와 글자를 차례로 적습니다. 뒤에서 앞으로 칠해 가리는 이 방식을 그래픽스에서는 화가 알고리즘이라고 부릅니다.

렌더링 단계 속 페인트의 몫

이 소절은 페인트가 앞 단계에서 무엇을 받아 뒤 단계에 무엇을 넘기는지 봅니다.

브라우저는 서버가 보낸 글을 읽어 화면을 그립니다. 내용과 구조는 HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어)로 적혀 옵니다. 색과 크기 같은 생김새는 CSS(Cascading Style Sheets, 캐스케이딩 스타일 시트)로 적혀 옵니다.

이 글을 그림으로 바꾸는 일을 렌더링이라고 부릅니다. 브라우저 안에서 이 일을 맡은 부분이 렌더링 엔진입니다. 렌더링 엔진은 이 일을 여러 단계로 나눠 차례로 처리합니다.

페인트 바로 앞 단계는 레이아웃입니다. 레이아웃은 요소마다 화면의 어느 좌표에 얼마만 한 크기로 놓일지 계산합니다. 페인트는 이 결과를 받습니다. 좌표와 크기를 모르면 어디에 칠할지 정할 수 없습니다.

레이아웃이 정한 것은 좌표와 크기뿐입니다. 이것만으로는 화면에 아무것도 안 나옵니다. 요소끼리 겹치면 누가 위에 보일지도 아직 안 정해졌습니다. 칠할 것과 그 순서를 정해 주는 단계가 없으면 화면은 빈 채로 남습니다.

페인트 뒤에는 두 단계가 더 있습니다. 먼저 래스터화가 그리기 명령을 픽셀로 바꿉니다. 명령 목록이 실제 점의 색으로 바뀌는 것은 이 단계입니다.

브라우저는 페이지 전체를 한 장에 그리지 않을 때가 많습니다. 자주 바뀌는 부분을 떼어 따로 그려 둡니다. 이렇게 따로 그린 판 한 장을 레이어라고 부릅니다.

마지막 단계인 합성은 레이어들을 앞뒤 순서대로 겹쳐 한 화면을 만듭니다. 레이어 하나만 바뀌었으면 나머지는 다시 칠하지 않습니다. 겹치기만 다시 하면 됩니다.

flowchart TD
    L["레이아웃 · 요소마다 좌표와 크기"] --> P["페인트 · 무엇을 어떤 순서로 칠할지"]
    P --> R["래스터화 · 명령을 픽셀로"]
    R --> C["합성 · 레이어를 겹친다"]
    C --> V["화면"]

그림에서 페인트가 넘기는 것은 아직 픽셀이 아닙니다.

그리기 명령 목록

페인트의 결과물은 그림이 아니라 명령의 목록입니다. 「이 좌표에 이 크기로 이 색 사각형을 칠하라」 같은 명령이 칠할 순서대로 늘어섭니다. 이 목록을 디스플레이 리스트라고 부릅니다.

아래는 흰 페이지에 파란 버튼 하나가 놓인 모습을 명령으로 적은 것입니다. 렌더링 엔진이 실제로 쓰는 표기가 아니라 뜻만 옮겼습니다. 괄호 안은 왼쪽 위 모서리의 좌표입니다. 그 옆은 너비와 높이입니다.

1  사각형 칠하기   (0, 0)     800 × 600   흰색
2  사각형 칠하기   (20, 20)   120 × 40    파랑
3  테두리 두르기   (20, 20)   120 × 40    진한 파랑
4  글자 쓰기       (44, 46)   "확인"      흰색

목록은 위에서 아래로 실행됩니다. 1번의 흰 배경이 먼저 깔리고 4번의 글자가 맨 위에 올라갑니다. 4번을 2번보다 먼저 실행하면 글자는 파란 사각형에 덮여 안 보입니다.

픽셀을 바로 칠하지 않고 목록을 먼저 만드는 데는 까닭이 있습니다. 목록은 픽셀보다 훨씬 작습니다. 같은 목록으로 픽셀을 몇 번이든 다시 뽑을 수 있습니다. 화면을 확대하면 좌표만 키워 목록을 다시 실행하면 됩니다.

픽셀로 바꾸는 일은 시간이 많이 듭니다. 목록으로 떼어 두면 이 일을 그림 처리에 특화된 장치인 GPU(Graphics Processing Unit, 그래픽 처리 장치)에 맡길 수 있습니다. 브라우저의 주 작업 흐름인 메인 스레드는 그동안 다른 일을 합니다.

페인트라는 이름을 픽셀을 칠하는 일까지 넓게 쓰기도 합니다. 둘을 나눠 부를 때는 명령 목록을 만드는 쪽이 페인트입니다. 픽셀로 바꾸는 쪽은 래스터화입니다.

칠하는 순서

이 소절은 페인트가 칠할 순서를 정하는 규칙을 봅니다. 요소 하나 안의 순서와 겹친 요소 사이의 순서로 나눠 봅니다.

요소 하나 안에서는 순서가 정해져 있습니다. 배경색을 먼저 칠합니다. 그 위에 배경 이미지를 올린 다음 테두리를 두릅니다. 그 안의 글자와 자식 요소는 그 위에 그립니다.

요소끼리 겹칠 때는 대체로 HTML 에 뒤에 적힌 요소가 나중에 칠해져 위에 보입니다. 이 순서를 바꿀 때 쓰는 CSS 속성이 z-index입니다. z-index 값이 큰 요소가 나중에 칠해져 앞에 보입니다. 이 속성은 주로 position 속성으로 위치를 따로 지정한 요소에 줍니다.

z-index 가 페이지 전체에서 겨루지는 않습니다. 요소 가운데 일부는 제 자손들만의 층 묶음을 새로 엽니다. 이 묶음을 쌓임 맥락이라고 부릅니다. 자손의 z-index 는 그 묶음 안에서만 순서를 정합니다.

새 쌓임 맥락을 여는 흔한 경우는 둘입니다. 위치를 지정하고 z-index 를 준 요소가 새 묶음을 엽니다. 투명도를 1보다 낮춘 요소도 새 묶음을 엽니다.

그래서 z-index 를 999 로 올려도 묶음 밖으로 못 나오는 일이 생깁니다. 아래 그림에서 상자 A 와 B 는 형제입니다. 둘은 각자 쌓임 맥락을 엽니다.

그림의 테두리 하나가 묶음 하나입니다. 화살표는 칠하는 순서입니다.

flowchart TD
    subgraph A["상자 A 의 묶음 · z-index 1"]
        A1["A 의 자식 · z-index 999"]
    end
    subgraph B["상자 B 의 묶음 · z-index 2"]
        B1["B 의 내용"]
    end
    A -->|"1 이 2 보다 작아 A 를 먼저 칠한다"| B

A 의 자식은 999 를 받았지만 A 의 묶음 안에서만 겨룹니다. 묶음끼리는 A 의 1 과 B 의 2 를 견줍니다. B 가 나중에 칠해지므로 A 를 자식까지 전부 덮습니다.

다시 칠하기

페이지는 한 번 칠하고 끝나지 않습니다. 이 소절은 무엇이 바뀔 때 페인트가 다시 실행되는지를 코드 세 줄로 봅니다.

브라우저는 HTML 을 읽어 요소들이 서로 품는 나무 구조를 만듭니다. 이 구조를 DOM(Document Object Model, 문서 객체 모델)이라고 부릅니다. 자바스크립트 코드는 DOM 의 요소를 고쳐 화면을 바꿉니다. 아래 세 줄은 한 상자의 생김새를 하나씩 바꿉니다. 주석은 다시 실행되는 첫 단계입니다.

JavaScript
box.style.color = "red";   // 페인트부터
box.style.width = "50%";   // 레이아웃부터
box.style.opacity = "0.5"; // 대개 합성만

첫 줄은 글자 색만 바꿉니다. 좌표와 크기는 안 바뀌므로 레이아웃을 건너뛰고 페인트부터 다시 실행됩니다. 이렇게 다시 칠하기만 하는 것을 리페인트라고 부릅니다.

둘째 줄은 너비를 바꿉니다. 너비가 바뀌면 글이 접히는 줄 수와 이웃 요소의 좌표까지 달라집니다. 그래서 레이아웃을 다시 계산합니다. 페인트도 다시 실행됩니다. 이 재계산을 리플로우라고 부릅니다.

셋째 줄은 투명도를 바꿉니다. 이 상자가 레이어에 따로 그려져 있으면 그 레이어를 흐리게 겹치기만 하면 됩니다. 레이어에 든 요소는 투명도나 위치 이동만 바뀔 때 다시 칠하지 않습니다. 합성만 다시 합니다.

다시 칠할 때도 페이지 전부를 칠하지는 않습니다. 렌더링 엔진은 바뀐 요소가 차지한 영역에 「다시 칠할 곳」이라는 표시를 해 둡니다. 다음 그림을 그릴 때 대개 그 영역만 다시 칠합니다. 낡은 결과를 버리라고 이렇게 표시하는 일을 페인트 무효화라고 부릅니다.

페인트가 오래 걸리는 경우

이 소절은 페인트가 화면을 끊기게 만드는 경우를 봅니다. 그다음 그 비용을 덜어 내는 방법과 그 방법의 대가를 봅니다.

화면은 1초에 여러 번 새 그림으로 바뀝니다. 새로 바뀌는 그림 한 장을 프레임이라고 부릅니다. 스크롤과 애니메이션이 부드러우려면 다음 프레임이 나가기 전에 페인트까지 끝나야 합니다.

스레드는 운영체제가 따로따로 돌려 주는 실행 흐름 하나입니다. 브라우저도 일을 여러 스레드에 나눠 맡깁니다.

페인트는 대개 자바스크립트 코드와 같은 스레드에서 번갈아 실행됩니다. 이 스레드가 앞에서 말한 메인 스레드입니다. 그래서 페인트가 길어지면 코드 실행이 밀립니다. 코드가 길어지면 페인트가 밀립니다.

칠할 넓이가 넓을수록 오래 걸립니다. 넓은 그림자나 흐림 효과가 붙은 요소는 픽셀로 바꾸는 비용이 큽니다. 이런 요소를 애니메이션으로 바꾸면 프레임마다 다시 칠해야 합니다.

흔한 처방은 자주 움직이는 요소를 레이어로 떼어 두는 것입니다. 움직임을 위치 이동과 투명도 변화로만 나타내면 페인트 없이 합성만 다시 합니다. CSS 에서 위치 이동은 transform 속성으로, 투명도는 opacity 속성으로 줍니다.

will-change 속성은 이 요소가 곧 바뀐다고 브라우저에 미리 알립니다. 브라우저는 이 힌트를 보고 그 요소를 레이어로 미리 떼어 둘 수 있습니다.

대가는 메모리입니다. 레이어는 저마다 픽셀을 한 장씩 따로 들고 있습니다. 레이어를 많이 떼면 그만큼 메모리를 더 씁니다. 겹칠 레이어도 늘어 합성에 시간이 더 듭니다.

이름에 페인트가 든 성능 지표

웹 페이지의 속도를 재는 지표 가운데 여럿이 이름에 페인트를 달고 있습니다. 이 지표들이 말하는 페인트는 넓은 뜻입니다. 무언가가 칠해져 사용자 눈에 보인 때를 가리킵니다.

지표 재는 것
첫 페인트(First Paint, FP) 페이지에 무엇이든 처음 칠해진 때
첫 콘텐츠풀 페인트(First Contentful Paint, FCP) 글자나 이미지 같은 내용이 처음 칠해진 때
최대 콘텐츠풀 페인트(Largest Contentful Paint, LCP) 화면에서 가장 큰 글 덩어리나 이미지가 칠해진 때
다음 페인트까지의 상호작용(Interaction to Next Paint, INP) 클릭이나 키 입력 뒤 화면이 다음으로 칠해지기까지 걸린 시간

표의 위 세 지표는 페이지를 처음 열 때를 잽니다. 그래서 서버와도 닿아 있습니다. 서버가 HTML 을 늦게 보내면 첫 페인트도 늦어집니다.

서버 사이드 렌더링은 서버에서 HTML 을 완성해 보내는 방식입니다. 이 이름의 렌더링은 화면을 칠하는 일이 아니라 HTML 을 만들어 내는 일을 뜻합니다. 브라우저는 받자마자 내용을 칠할 수 있습니다. 그만큼 첫 콘텐츠풀 페인트가 앞당겨집니다.

브라우저 밖의 페인트

창을 띄우는 데스크톱 프로그램에도 페인트가 있습니다. 가려져 있던 창이 드러나거나 창 크기가 바뀌면 운영체제가 프로그램에게 그 영역을 다시 칠하라고 알립니다. 프로그램은 이 알림을 받고 제 모습을 그립니다.

이런 프로그램의 화면을 만드는 라이브러리를 GUI 툴킷이라고 부릅니다. GUI 는 Graphical User Interface, 그래픽 사용자 인터페이스의 줄임말입니다. 창과 단추 같은 화면 부품을 만들어 주는 것이 이 라이브러리의 일입니다.

자바의 Swing 이 GUI 툴킷의 한 예입니다. Swing 에서는 화면 부품 클래스의 paintComponent 메서드를 오버라이드해서 그리는 코드를 넣습니다. 부품을 다시 칠해야 할 때 이 메서드가 불립니다.

관련 항목

페인트 앞뒤로 도는 렌더링 단계

렌더링 · 렌더링 엔진 · DOM · CSSOM · 스타일 계산 · 렌더 트리 · 레이아웃 · 래스터화 · 합성 · 중요 렌더링 경로

페인트가 만들어 넘기는 결과물

디스플레이 리스트 · 합성 레이어 · 픽셀 · 비트맵 · 프레임

페인트 순서를 정하는 CSS 규칙

CSS · 박스 모델 · z-index · 쌓임 맥락 · 화가 알고리즘

페인트를 다시 돌게 하는 변경과 문제

리페인트 · 리플로우 · 페인트 무효화 · 레이아웃 스래싱 · 강제 동기 레이아웃

페인트 부담을 덜어 주는 수단

will-change · CSS 트랜스폼 · 하드웨어 가속 · GPU · 컴포지터 스레드

페인트를 돌리는 실행 구조

메인 스레드 · 스레드 · 이벤트 루프 · requestAnimationFrame · 프레임 레이트

페인트 시점을 재는 지표

첫 페인트 · 첫 콘텐츠풀 페인트 · 최대 콘텐츠풀 페인트 · 다음 페인트까지의 상호작용 · 코어 웹 바이탈 · 서버 사이드 렌더링

페인트와 이름이 겹치는 개념

GUI 툴킷 · Swing · 캔버스 · 다시 그리기

다른 이름: paint · painting · 페인팅