CSS Paint API로 동적 배경을 만드는 방법


원문: https://www.webtips.dev/how-to-make-dynamic-backgrounds-with-the-css-paint-api

UnsplashPaweł Czerwiński의 사진.

현대의 웹 애플리케이션은 많은 이미지가 사용되며 다운로드되는 용량의 대부분을 차지한다. 이를 최적화하면 웹페이지의 성능을 효과적으로 향상시킬 수 있다. 기하학적 모양의 배경 이미지를 사용하는 경우, CSS Paint API를 사용하여 프로그래밍 방식으로 이를 대체할 경우 효과적으로 웹페이지의 성능을 향상시킬 수 있다.

이 튜토리얼에서 CSS Paint API의 기능을 살펴보고, 해상도 영향을 받지 않는 동적 배경을 만드는 방법을 알아보자. 먼저 튜토리얼 결과를 살펴보자.


프로젝트 설정

먼저 새 index.html 파일을 생성하고 다음 코드로 채워보자.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>🎨 CSS Paint API</title>
        <link rel="stylesheet" href="styles.css" />
    </head>
    <body>
        <textarea class="pattern"></textarea>
        <script>
            if ('paintWorklet' in CSS) {
                CSS.paintWorklet.addModule('pattern.js');
            }
        </script>
    </body>
</html>

다음 몇 가지 사항에 유의해야 한다.

  • 13번째 줄에서 paintWorklet을 로드한다. 글로벌 브라우저 지원율은 현재 63%이다. 그렇기 때문에 개발되는 환경에서 먼저 paintWorklet이 지원되는지부터 확인해야 한다.
  • 데모를 위해 textarea을 캔버스로 사용하고 있으므로, textarea의 크기를 조정하여 패턴이 어떻게 다시 그려지는지 확인할 수 있다.
  • 마지막으로, paintWorklet을 등록할 pattern.js와 몇 가지 스타일을 정의할 수 있는 styles.css를 만들어야 한다.

worklet이란 무엇인가?

paintWorklet은 그려야 하는 것을 정의하는 클래스로서 캔버스 요소와 유사하게 동작한다. 만약 캔버스 요소에 대해 이미 알고 있다면 코드는 익숙해 보일 것이다. 하지만 100% 동일하지는 않다. 예를 들면, 텍스트 랜더링은 아직 paintWorklet에서 지원하지 않는다.

이제 CSS 스타일을 정의해보자. CSS에서 paintWorklet의 사용을 지정할 수 있다.

.pattern {
    width: 250px;
    height: 250px;
    border: 1px solid #000;

    background-image: paint(pattern);
}

텍스트 영역을 더 잘 확인할 수 있도록 검은색 테두리를 추가했다. paintWorklet 작업물을 참조하려면 paint(paintWorklet 이름)background-image 속성 값으로 전달해야 한다.pattern은 다음 단계에서 살펴보자. 아직 정의하지 않았으니 다음 단계에서 살펴보자.

worklet 정의

pattern.js파일을 만들고 다음 내용을 추가하자.

class Pattern {
    paint(context, canvas, properties) {
        
    }
}

registerPaint('pattern', Pattern);

registerPaint 메서드로 paintWorklet을 등록할 수 있다. 첫 번째 매개변수는 CSS에서 참조하기 위한 네이밍이고, 두 번째 매개변수는 캔버스에 그릴 것을 정의하는 클래스이다.이 클래스는 세 가지 매개변수를 사용하여 구현되는 paint 메서드를 가진다.

  • context - CanvasRenderingContext2D API의 하위 집합을 구현하는 PaintRenderingContext2D 개체를 반환한다.
  • canvas - 너비와 높이의 두 가지 속성만 갖는 PaintSize 객체다.
  • properties - CSS 속성과 값을 읽는 데 사용되는 StylePropertyMapReadOnly 객체가 반환된다.

직사각형 그리기

이제, 직사각형을 그려보자. paint 메서드에 다음 코드를 추가한다.

paint(context, canvas, properties) {
    for (let x = 0; x < canvas.height / 20; x++) {
        for (let y = 0; y < canvas.width / 20; y++) {
            const bgColor = (x + y) % 2 === 0 ? '#FFF' : '#FFCC00';
  
            context.shadowColor = '#212121';
            context.shadowBlur = 10;
            context.shadowOffsetX = 10;
            context.shadowOffsetY = 1;
  
            context.beginPath();
            context.fillStyle = bgColor;
            context.rect(x * 20, y * 20, 20, 20);
            context.fill();
        }
    }
}

여기서는 중첩 루프를 만들어 캔버스의 폭과 높이를 이용한 계산을 한다. 직사각형의 크기가 20이므로 높이와 폭을 모두 20으로 나눈다.

네 번째 라인에서 나머지 연산자를 이용하여 두 색상 사이를 전환한다. 깊이 표현을 위해 shadow속성을 몇 개 추가했다. 마지막으로 직사각형을 그린다. 브라우저에서 파일을 열면 아래와 같이 보여야 한다.


배경을 동적으로 만들기

textarea의 크기를 조정할 때 Paint API가 다시 그리는 걸 엿볼 수 있지만, 안타깝게도 그래도 여전히 정적이다. 우리가 변경 가능한 사용자 정의 CSS속성을 추가하여 좀 더 동적인 배경을 만들어 보자.

styles.css 파일을 열고 아래 코드를 추가하자.

 .pattern {
     width: 250px;
     height: 250px;
     border: 1px solid #000;

     background-image: paint(pattern);
+    --pattern-color: #FFCC00;
+    --pattern-size: 23;
+    --pattern-spacing: 0;
+    --pattern-shadow-blur: 10;
+    --pattern-shadow-x: 10;
+    --pattern-shadow-y: 1;
 }

사용자 정의 CSS 속성은 --로 접두사를 붙여 정의할 수 있다. 보통은 var()함수를 이용하여 사용할 수 있다. 하지만 우리는 paintWorklet에서 사용할 것이다.

CSS로 지원 확인하기

CSS를 통해 Paint API가 지원되는지 확인할 수도 있다. 이를 위한 두 가지 옵션이 있다.

  • @supports를 이용하여 규칙을 보호.
  • 대체 배경 이미지 사용
/* 첫번째 옵션 */
@supports (background: paint(pattern)) {
  /**
   * 이 부분이 실행되면 Paint API가 지원됨을 의미한다
   **/
}

/**
 * 두번째 옵션
 * Paint API가 지원되는 경우 후자의 규칙으로 재정의 된다
 * 그렇지 않을 경우 url()이 적용된다
 **/
.pattern {
  background-image: url(pattern.png);
  background-image: paint(pattern);
}

paintWorklet의 매개변수에 접근하기

pattern.js 내에서 매개 변수를 읽으려면 아래처럼 paintWorklet을 정의하는 클래스에 새 메서드를 추가해야 한다.

class Pattern {
    // paintWorklet은 `inputProperties` 메소드가 반환하는 모든 항목에 접근할 수 있다.
    static get inputProperties() { 
        return [
            '--pattern-color',
            '--pattern-size',
            '--pattern-spacing',
            '--pattern-shadow-blur',
            '--pattern-shadow-x',
            '--pattern-shadow-y'
        ]; 
    }
}

paint 메서드 내에서 properties.get를 사용하여 속성에 액세스 할 수 있다.

paint(context, canvas, properties) {
    const props = {
        color: properties.get('--pattern-color').toString().trim(),
        size: parseInt(properties.get('--pattern-size').toString()),
        spacing: parseInt(properties.get('--pattern-spacing').toString()),
        shadow: {
            blur: parseInt(properties.get('--pattern-shadow-blur').toString()),
            x: parseInt(properties.get('--pattern-shadow-x').toString()),
            y: parseInt(properties.get('--pattern-shadow-y').toString())
        }
    };
}

properties.getCSSUnparsedValue를 반환하기 때문에, 색상은 문자열로 변환해야 하며 나머지는 모두 숫자로 변환해야 한다.

좀 더 읽히기 쉬운 코드를 만들기 위해 파싱을 처리하는 두 가지 새로운 함수를 만들었다.

paint(context, canvas, properties) {
    const getPropertyAsString = property => properties.get(property).toString().trim();
    const getPropertyAsNumber = property => parseInt(properties.get(property).toString());

    const props = {
        color: getPropertyAsString('--pattern-color'),
        size: getPropertyAsNumber('--pattern-size'),
        spacing: getPropertyAsNumber('--pattern-spacing'),
        shadow: {
            blur: getPropertyAsNumber('--pattern-shadow-blur'),
            x: getPropertyAsNumber('--pattern-shadow-x'),
            y: getPropertyAsNumber('--pattern-shadow-y')
        }
    };
}

이제 for 루프에 있는 모든 속성 값을 해당하는 prop 값으로 바꾸기만 하면 된다.

for (let x = 0; x < canvas.height / props.size; x++) {
    for (let y = 0; y < canvas.width / props.size; y++) {
        const bgColor = (x + y) % 2 === 0 ? '#FFF' : props.color;

        context.shadowColor = '#212121';
        context.shadowBlur = props.shadow.blur;
        context.shadowOffsetX = props.shadow.x;
        context.shadowOffsetY = props.shadow.y;

        context.beginPath();
        context.fillStyle = bgColor;
        context.rect(x * (props.size + props.spacing),
                     y * (props.size + props.spacing), props.size, props.size);
        context.fill();
    }
}

이제 브라우저로 돌아가 속성을 변경해보자.

개발자 도구를 통한 배경 편집

요약

CSS Paint API가 왜 유용할까? 사용 사례가 있을까?

분명한 것은 API의 도움으로 응답의 크기를 줄인다는 것이다. 이미지 사용을 제거함으로써 하나의 네트워크 요청과 몇 킬로바이트의 용량을 절약하여 성능을 향상시킬 수 있다.

다른 예로, DOM 요소를 사용하는 복잡한 CSS효과의 경우 페이지의 노드 수도 줄일 수 있다. Paint API를 사용하여 복잡한 애니메이션을 만들 수 있으므로 비어있는 노드를 추가할 필요가 없다.

필자가 생각하는 가장 큰 이점은 정적 배경 이미지보다 훨씬 더 많은 사용자 정의가 가능하다는 것이다. API는 해상도에 영향이 없는 이미지를 생성하므로 특정한 화면 크기를 놓치는 것에 대해 걱정할 필요가 없다.

CSS Paint API는 아직 브라우저 지원률이 높지 않으므로 적용 전에 polyfill 사용을 고려해야 한다. 튜토리얼의 완성된 프로젝트를 보려면 이 깃헙 저장소를 참고하기 바란다.

이 글을 읽어줘서 감사하다. 즐거운 코딩하길 바란다!

김진우2021.01.25
Back to list