그래픽 시스템을 만들자 -1-

개요

그래픽은 기본적으로 화면에 점을 찍는 것입니다. 화면은 물리적인 픽셀의 위치가 있으므로 사실 정확한 숫자로 지정해줘야합니다.
하지만 이렇게 그림을 만들면 여러가지 불편한 점이 생깁니다.

예를 들어 ‘화면에 꽉차는 사각형’ 같은 표현은 사람에게는 좋지만 픽셀의 위치로 계산하려면

  1. 화면의 크기를 먼저 알아내서
  2. 그 크기에 맞춰 픽셀의 위치를 정해야하는 것이죠.

게다가 데탑의 브라우저처럼 화면의 크기가 자유롭게 변하는 경우는 화면의 크기가 변할 때마다 다시 재계산하여 그려야합니다.
이러한 문제는 꽉차는 것 뿐만 아니라 다음과 같은 모든 상황에 해당됩니다.

  • 부모의 크기에 절반만한 사각형
  • 첫 번째 사각형의 바로 옆에 붙어있는 사각형
  • 특정 공간에서 자동으로 줄바꿈이 되면서 배치되는 사각형

위의 표현은 사람에게 편리한 기능을 제공하여 직접 픽셀의 위치를 계산하는 대신 내부에 있는 계산기가 알아서 픽셀의 위치와 크기를 정해주는 시스템이라 할 수 있습니다.

즉 그래픽 시스템이란 그림을 그리는 시스템이 아니라

여러 조건이나 상황에 맞춰 x, y, width, height를 적절히 계산해주는 계산기

라 할 수 있겠습니다.
이 계산만 잘 끝나면 이 계산 결과를 이용해서 실제 그림을 그리는 것은 DOM일 수도 있고 캔버스나 웹지엘, 안드로이드, 아이폰이 될 수도 있을 것입니다.

그래픽시스템은 계산기에요?
응.
계산기가 어떻게 그림을 그리는 거에요?
그건 좀 있다가 생각해보자 ^^;


목적

헌데 이런 건 대체 왜 만들어 보는 걸까요? 일단 고수준 그래픽 시스템을 만들 수 있게 되면 다음과 같은 장점이 생깁니다.

  • 주어진 그래픽 시스템이 느린 경우 성능 상 대안
  • 시스템에서 제공하지 않는 특별한 레이아웃 사용가능
  • 여러 환경에서 일관된 방법으로 그래픽을 사용

그 외에도 다양한 장점을 제공합니다만 꼭 이러한 게 아니라도 이미 사용하고 있는 그래픽 시스템을 보다 잘 이해하여 성능을 최대한 끌어낼 수 있게 됩니다.

저는 컨스트레인트 레이아웃이 어려워요.
아마 누구나 그럴거야.
그래픽 시스템을 배우면 이것도 잘하게 되나요?
잘하게 될지는 모르지만, 좀 더 이해는 될거야 ^^;


그림이 그려지는 2단계

컴터의 그림이란 기본적으로 화면에 점을 찍는 것이지만, 아무렇게나 점을 찍는 것으로는 사람이 통제하기 어렵습니다.
예를들어 사각형을 그릴려면 일정한 사각 영역을 정한 뒤, 그 안이나 외곽선에 점을 찍어야 사각형이라는 모양을 나타낼 수 있죠.

따라서 그리기 단계를 나눠보면 그림을 그릴 영역을 정하는 단계와 그 영역에 점을 찍어가는 단계로 나누는 편이 훨씬 다루기 쉬워집니다.

  1. 영역을 정하는 단계 : 영역은 영어로 지오메트리(Geometry)라고 합니다. 따라서 지오메트리 단계라고도 하죠. 이 단계를 거치면 그림 그릴 대상의 x, y, width, height를 확정짓게 됩니다.
  2. 점을 찍는 단계 : 화면 상의 점을 나타내는 영어는 프레그먼트(Fragment)라고 합니다. 보통 이 단계를 프레그먼트 단계라 하죠. 이 단계에서 해당 영역을 칠하게 됩니다. 단색, 그라디언트, 이미지는 물론 심지어 텍스트까지도 색칠의 일종이라 할 수 있습니다.
먼저 그림 그릴 영역을 정하라고 했죠?
맞아 이제 거기에 색칠하면!


그래픽스 시스템의 최적화 요령

거의 모든 컴터에서 그림 그리기는 이 두 단계를 따릅니다. 그렇다보니 지오메트리 단계와 프레그먼트 단계에 대해 각 시스템별로 별명을 다르게 붙이는 경우가 많습니다.
예를들어 친숙한 HTML시스템에서는 지오메트리 단계를 리플로우(reflow)라 부르고 프레그먼트 단계는 리페인트(repaint)라 부릅니다.

이미 많은 HTML최적화에서 들어보셨겠지만 최대한 리플로우(지오메트리 단계)를 줄이라고 합니다. 리페인트는 이미 정해진 영역에 색칠만 다시 하는데 비해 리플로우는 영역을 재계산하기 때문이죠.
하지만 이것만으로는 정확히 리플로우가 더 큰 문제를 일으킨다는 것을 이해하기 힘듭니다.

  1. 영역을 재계산한다는 뜻은 자동으로 리페인트도 하겠다는 뜻입니다.
  2. 게다가 하나의 영역을 재계산하면 이에 영향을 받는 영역이 생겨 많은 여파가 일어납니다.

따라서 컴터에서 그림을 그릴 때 효과적인 전략은 사실 어디에서나(아이폰이든, 안드로이드든, HTML이든) 비슷한 것입니다.

  1. 되도록이면 지오메트리 단계의 재계산이 일어나지 않게 합니다.
  2. 만약 지오메트리 재계산이 일어나더라도 그 여파를 최소화시켜 다른 영역을 재계산할 필요가 없게 격리합니다.

일반적으로 사람에게 더욱 친숙한 레이아웃 시스템일 수록 더욱 큰 지오메트리 재계산을 일으키게 되므로 사용자의 주의를 많이 필요로 하게 됩니다.

컴터나 스마트폰은 점점 더 좋아지고 있잖아요.
그래도 그림 그리기는 무거워.
얼마나 무거워요?
컴터 성능 대부분을 그림 그리는데 쓰지.
멍! 그렇군요.


제작할 그래픽 시스템에 대해

우선 지오메트리 단계를 처리하는 시스템을 만들어야 합니다. 일단 지오메트리만 잘 계산된다면 그 안을 색칠하는 프레그먼트 단계는 그 다음에 생각하면 됩니다.

  1. 점진적으로 지오메트리를 구성하는 기반을 만들고
  2. HTML등에서 사용하는 다양한 지오메트리 기능을 번역해서 만들어 봅니다.
  3. 이후 프레그먼트 시스템과 결합하여 실제 그림을 그려봅니다.

  4. 처음에는 타입스크립트를 이용해 브라우저에서 실습할 수 있게 만들어갑니다.

  5. 이 후 기본 구조가 완성된 이후에는 코틀린으로 번역하여 안드로이드용도 만들어보고
  6. 스위프트로 번역하여 아이폰용도 만들어보죠
타입스크립트, 코틀린, 스위프트라니 멍!
아주 나중이니까 너무 걱정하지마 ^^
멍! 멍!


컨테이너와 그 안의 아이템

지오메트리는 크기와 위치를 정하는 과정이지만 사람이 사고하기 편한 개념을 제공해줘야합니다.
대표적인 개념으로 부모, 자식이라는 개념을 쓰죠. 여기서는 부모 자식보다 보다 포괄적인 개념인 컨테이너와 아이템이라는 단어를 사용하겠습니다.
컨테이너와 아이템이라는 개념을 사용하는 이유는 뭘까요?

화면에 그려야할 많은 내용을 컨테이너별로 그룹지어서 그 안에 속한 아이템만 정리하는 식으로 생각하면 훨씬 쉽게 생각할 수 있기 때문입니다.
웹사이트라면 상단 영역, 좌메뉴 영역, 본문 영역으로 나눠서 생각하는게 더 편한 것과 같은 이치죠.

이렇게 컨테이너와 아이템을 사용하면 암묵적으로 다음과 같은 어려운 개념이 추가로 나오게 됩니다.

컨테이너를 기준으로 하는 아이템

보통 아이템은 컨테이너를 기준으로 하는 좌표를 사용하게 됩니다. 따라서 컨테이너를 이동하면 컨테이너의 아이템도 동시에 따라서 이동하게 되고, 컨테이너의 크기를 바꾸면 아이템의 크기도 영향을 받는 식입니다.

컨테이너이자 아이템

컨테이너는 그 컨테이너를 소유하는 컨테이너 입장에서는 아이템이죠. 반대로 컨테이너 안의 아이템이라 할지라도 그 아이템이 소유하고 있는 아이템들 입장에서는 컨테이너입니다.
이렇듯 컨테이너와 아이템은 절대적인 개념이 아니라 무엇을 기준으로 보는가에 대한 상대적인 개념입니다.
일반적으로 모든 아이템은 컨테이너이자 아이템이지만 오직 컨테이너로만 작동하는 진입점이 있기 마련입니다. HTML에서는 documentElement같은 것에 해당되는 것으로 모든 컨테이너의 최상위 컨테이너입니다.
이 최상위 컨테이너는 오직 컨테이너로만 작동하고 누군가의 아이템이 되지는 않습니다.

컨테이너와 아이템이 상호 협력하여 위치와 크기가 결정

컨테이너가 홀로 크기를 확정짓는 경우도 있을 것입니다. 예를들어 이 컨테이너는 500×500이다라고 확정짓는 것이죠.
그렇다면 여기에 600*600처럼 더 큰 아이템을 담는 경우 컨테이너는 어떻게 될까요? 보통 예상할 수 있는 것은 다음의 세가지입니다.

  1. 컨테이너보다 큰 영역은 감춘다(hidden)
  2. 컨테이너 안에 스크롤바를 만들어 준다(scroll)
  3. 컨테이너보다 자식이 크면 자식 크기만큼 크기를 더 키워준다(auto)

바로 이러한 세 가지 정책이 HTML에서는 overflow라는 css속성으로 존재하며 거기에 값으로 위에 세 가지를 지정할 수 있습니다.

이렇듯 컨테이너는 물론 아이템과 무관하게 자기의 크기를 확정지을 수도 있습니다. 하지만 div의 기본값에 width, height를 따로 설정하지 않은 경우는 어찌될까요.
안에 아이템이 어떻게 들어있냐에 따라 width, height가 달라지게 됩니다. 즉 자신의 크기를 아이템을 배치한 결과 필요한 크기에 맞춰 변경한 것이죠.

이렇듯 컨테이너의 크기는 아이템의 위치 및 크기에 따라 결정될 수도 있습니다.

멍!
아인, 그만 멍멍해 ^^;


사각형 영역

지오메트리를 구성하는 방법은 다양합니다. 그래프를 그리는 D3사이트에 가면 아래 같은 그림들이 보입니다.

특히 빨간박스로 표시한

  • bubble Chart는 지오메트리를 원의 크기와 외곽선을 기준으로 결정하고 있으며,
  • Voronoi Diagram은 복잡한 폴리곤영역을 결합하여 지오메트리를 결정하고 있습니다.

이렇듯 특수한 공간을 배치하는 지오메트리가 없는 것은 아닙니다. 하지만 이 연재에서는 사각형을 기준으로 배치하는 지오메트리 시스템만 다룰 예정입니다.
사각형은 덧셈, 뺄셈만으로 쉽게 지오메트리를 결정할 수 있고 직관적으로 이해하기 쉽기 때문에 그래픽 입문에 매우 좋은 시스템입니다.
뿐만 아니라 DOM을 비롯한 대부분의 그래픽스 시스템은 사각형 지오메트리를 기본으로 채용하고 있습니다.

이러한 사각형은 앞으로 계속 사용하게 될 것이므로 간단히 클래스를 정의하고 가겠습니다.

//Rect.ts

//1
type arg = {
  x?:number,
  y?:number,
  width?:number,
  height?:number
};

export default class Rect{
  //2
  private static base = new Rect(0, 0, 0, 0);

  //3
  static new(
    {x, y, width, height}:arg= {}, //4
    {x:baseX, 
     y:baseY, 
     width:bWidth, 
     height:baseHeight} = Rect.base //5
  ){
    //6
    return new Rect(
      x !== undefined ? x : baseX,
      y !== undefined ? y : baseY,
      w !== undefined ? w : baseWidth,
      h !== undefined ? h : baseHeight
    );
  }

  //7
  private constructor(
    x:number, 
    y:number, 
    width:number, 
    height:number
  ){
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
  }

  readonly x;
  readonly y;
  readonly width;
  readonly height;

  //8
  equals(rect: Rect) {
    return rect.x === this.x && 
      rect.y === this.y && 
      rect.width === this.width && 
      rect.height === this.height;
  }
}

Rect클래스는 불변 값 객체입니다. 하나의 사각형을 나타내기 위해 x, y, width, height라는 속성을 갖고 있으며 생성자는 외부에 공개하지 않은 체 정적함수인 Rect.new를 통해서만 만들 수 있게 제약을 걸어두고 있습니다.
이렇게 생성자를 외부에 노출하지 않고 정적함수로 인스턴스를 만들게 강제하면 나중에 객체풀링이나 캐쉬 정책등에 활용할 수 있게 됩니다.
코드에 등장하는 각 번호별 주석에 따라 상세히 코드를 설명해보겠습니다.

  1. 처음 정의되는 arg타입은 Rect.new가 받아들일 첫 번째 인자를 정의합니다. 이 정의에 따라 x, y, width, height가 전부 선택적으로 존재할 수도, 존재하지 않을 수도 있는 객체를 의미합니다. 원하는 속성만 지정하고 나머지는 기본값이 대체되게 하려는 의도로 정의되어있습니다.
  2. base라는 정적 속성은 Rect.new함수에서 x, y, width, height 중 지정하지 않은 속성에 대한 기본값을 주기 위해 정의한 객체입니다. 모든 값이 0으로 초기화 되어있습니다. 보통 null이나 undefined로 분기하지 않기 위해 기본값 객체를 만드는 일반적인 패턴으로 작성되었습니다.
  3. Rect.new메소드는 Rect인스턴스를 생성하기 위한 유일한 방법입니다. 인자로는 생성시 직접 값을 지정하는 첫 번째 arg타입의 객체와 두 번째로는 기존에 Rect로부터 값을 복사하기 위한 타겟 객체를 받습니다. 만약 두 번째 인자를 주지 않으면 자동으로 기본 객체인 Rect.base가 대체됩니다.
  4. Rect.new의 첫 번째 인자는 위에서 정의한 arg타입이며 x, y, width, height는 모두 undefined가 될 수 있습니다.
  5. 두 번째 인자는 Rect가 와야하지만 두 번째 인자를 전달하지 않은 경우는 Rect.base로 대체되어 첫 번째 인자에서 undefined인 속성에 대한 기본값으로 작동합니다.
  6. 불변 값 객체로 항상 새로운 Rect인스턴스를 반환합니다. 이 때 생성자에서 요구하는 x, y, width, height는 기본적으로 첫 번째 인자를 우선시 하지만, 첫 번째인자에서 전달받지 못한 경우는 두 번째 인자의 Rect객체로부터 받게 됩니다. 두 번째 인자를 주지 않은 경우는 언제나 0으로 초기화된 Rect.base가 지정되므로 없는 경우는 없습니다.
  7. 외부에서 직접적인 생성을 막기 위해 private으로 지정합니다.
  8. 값 객체 간 동일한지를 평가하기 위해 equals메소드를 만들고 각 속성을 비교합니다.

이제 이렇게 정의된 Rect클래스를 이용해 몇 가지 사각형을 만드는 예를 작성해보죠.

//가로, 세로 100인 사각형을 생성함
const rect1 = Rect.new({width:100, height:100});
console.log(rect1.x, rect1.y); //0, 0
console.log(rect1.width, rect1.height); //100, 100

//위에서 만든 rect1을 바탕으로 세로만 200으로 바꾼 새로운 사각형을 생성함
const rect2 = Rect.new({height:200}, rect1);
console.log(rect2.x, rect2.y); //0, 0
console.log(rect2.width, rect2.height); //100, 200

//같은 사각형인지 비교해보자.
console.log(rect1.equals(rect2)); //false
console.log(rect1.equals(Rect.new({}, rect1))); //true

간단히(?) 사각형을 정의하고 사용해 보았습니다. 앞으로 지오메트리를 계산하는 시스템을 만들고 나면, 그 지오메트리 계산의 결과는 하나의 Rect가 되어 표현될 것입니다.

사각형이 너무 어려워요.
원래 처음이 젤 어려운거야.
거짓말이죠? 멍!
진짜야 ^^


결론

그래픽 시스템에 대한 간단한 개요와 지오메트리 및 프레그먼트의 기초적인 개념을 익혀봤습니다.
또한 사각형 기반의 지오메트리를 위한 Rect클래스도 제작했네요. 다음 연재에서는 보다 자세한 지오메트리를 만들어보죠.

이번엔 사각형만 만드는 건가요?
사각형도 어렵다고 해서.
뭔가 속은거 같아요. 멍!
아인, 그만 짖어 ^^;