뛰어난 고객 경험 제공 위한 셔터스톡 ‘디지털 트랜스포메이션’ (6)
상태바
뛰어난 고객 경험 제공 위한 셔터스톡 ‘디지털 트랜스포메이션’ (6)
  • 윤현기 기자
  • 승인 2020.07.07 15:13
  • 댓글 0
이 기사를 공유합니다

자바스크립트·WebGL 양쪽서 평행한 필터 실행 유지…GPU 텍스처 최대한 재사용
HW·SW 조합서 관리할 수 있는 중복 타일들로 큰 이미지 분할…뛰어난 이미지 필터링 제공

[데이터넷] 2014년부터 디지털 트랜스포메이션을 추진한 셔터스톡은 현재 클라우드, 쿠버네티스 및 컨테이너 기술 등을 활용해 새로운 서비스들을 선보이고 뛰어난 고객 경험을 제공하고 있다. 그 외에도 독보적인 이미지 검색 기능, 컴퓨터 비전, API 등을 통해 전 세계 고객이 언제 어디서나 필요한 콘텐츠를 얻을 수 있도록 지원하고 있다. 본지에서는 6회에 걸쳐 셔터스톡의 IT 개발자들이 셔터스톡의 기술과 개발 과정을 추진한 과정을 연재한다. <편집자>

<연재 순서>

1. 셔터스톡의 디지털 트랜스포메이션 추진 이유 및 과정
2. 컴퓨터 비전과 딥러닝 기술 활용
3. 기술을 통해 인기 콘텐츠와 신규 콘텐츠 사이에 적절한 밸런스를 찾아 고객에게 제안하는 법
4. 어느 기업이나 API를 통해 셔터스톡과 콘텐츠 연계 가능
5. 프런트엔드 애플리케이션 통합 테스팅
6. 자체 이미지 편집 도구인 셔터스톡 에디터의 다중언어 지원 및 이미지 필터링 기능 개발 과정

안드레아 보가찌(Andrea Bogazzi) 셔터스톡 수석 엔지니어
안드레아 보가찌(Andrea Bogazzi)
셔터스톡 수석 엔지니어

셔터스톡의 자체 이미지 편집 도구인 ‘셔터스톡 에디터’는 소셜 미디어, 프레젠테이션 등을 위해 이미지를 쉽게 편집할 수 있도록 지원한다. 셔터스톡 에디터의 다중 언어 지원 과정과 이미지 필터링 방식을 어떻게 구현했는지에 대해 자세히 알아보도록 하자.

다중 언어 지원 과정

셔터스톡 에디터는 초기에 영어로만 출시되었다. 셔터스톡이 지원하는 21가지 언어에 대해 동일한 수준의 경험을 제공하기 위하여 셔터스톡 에디터가 해당 언어의 문자를 처리하는 방식을 조율해야 했다.

셔터스톡 에디터는 셔터스톡이 주요 기여자인 Fabric.js를 활용해 렌더링과 텍스트 편집을 관리한다. 텍스트 도구를 구현할 당시 Fabric.js는 1.7 버전으로 문자소(grapheme) 클러스터가 있는 언어에 대한 지원이 부족했다.

자바스크립트가 기본 다중언어 면(plane)에서 코드 포인트를 인식할 수는 있었지만, 문자소 클러스터를 처리할 수 있는 기본 헬퍼 기능이 없었다.

만약 ‘옛날 옛적에’라는 표현을 태국어로 해서 HTML 캔버스 요소에 페인트 작업을 요청하면, 다음과 같이 렌더링 되었다.

셔터스톡 에디터에서 렌더링 작업을 하기 위해 텍스트 객체에 사이즈를 제공하려면, 텍스트를 split()으로 조각내어 개별적으로 측정하여 문자열의 총 집합체의 크기를 구성한다.

텍스트를 조각내면 다음과 같은 코드포인트 리스트를 얻게 된다.

기존 텍스트가 9자로 구성된 것처럼 보였지만, 13자의 구별 가능한 문자로 나오는 것을 확인할 수 있다.

결과로 얻은 바운더리 박스는 적절한 크기보다 크고, 커서 위치는 개별 코드 포인트에 기반된 13자에 걸쳐 있다. 문자소에 기반된 9자 문자에 커서가 자리하고 있지 않다는 것은 타이핑 경험이 온전하지 못하다는 것을 의미한다.

커서가 위의 위치에 자리하고 있을 때 문자 a를 입력하면 다음과 같이 나타난다.

이것은 다소 운이 좋은 경우로, 그 외의 경우에는 문자가 문자소 클러스터 중앙에 위치할 수 있기에 이상한 결과가 나올 수도 있다.

다음 예에서는 다섯 번째 문자 다음에 문자를 입력하려고 커서를 오른쪽으로 옮기고자 한다. 그러나 Fabric.js에서 두 번째 문자는 윗글자 없이 기본 문자로만 인식된다.

해당 위치에 새로운 문자를 입력하면 텍스트 시퀀스가 깨져 기본 브라우저 렌더링 방법의 문맥적 의미가 손실되고 결합 문자를 개별적으로 렌더링하기 때문에 비논리적이다. 

태국어뿐만 아니라 이모지에도 비슷한 문제가 발생한다.

.split() 방식에서 벗어나 더 깊이 있는 문자소 클러스터를 인식하는 것은 단순히 몇 가지 코드 포인트 범위에서 도출할 수 있는 것이 아니기 때문에 쉽지 않은 작업이다. 다행히도 텍스트 문자열을 문자소 클러스터 배열로 나눌 수 있는 문자소 클러스터 및 라이브러리에 대한 유니코드 사양이 있다.

Fabric.js코드가 유니코드 문자와 코드 포인트를 읽을 수 있도록 새로 설정했다.

이렇게 Fabric.js를 변경해 셔터스톡 에디터는 영어와 동일한 수준으로 21가지 언어 및 이모지 또한 지원하게 됐다.

이미지 필터링 기능 방식

셔터스톡 에디터는 이미지 기반 디자인 도구에 필수적인 강력한 이미지 필터링 기능을 제공하고 있다. 다양한 브라우저 및 하드웨어적으로 제한된 상황에서 뛰어난 성능을 가진 이미지 필터링 기능을 어떻게 제공할 수 있는지 살펴보자.

- 셔터스톡 에디터 필터링 기능

[그림 1] 셔터스톡 에디터 이미지 필터링 인터페이스
[그림 1] 셔터스톡 에디터 이미지 필터링 인터페이스

셔터스톡 에디터는 ‘필터’라고 불리우는 프리셋과 대비 및 흐림과 같은 작업별 필터인 ‘효과’ 두 가지를 실행한다. 필터와 효과는 사용자 인터페이스에 보이는 것과 똑같이 적용된다. 셔터스톡 에디터는 현재 10개의 다양한 필터를 가지고 있어서 하나의 이미지에 최대 10개의 필터 단계를 수행해야할 수도 있다. 프리셋 자체가 일반적으로 몇 개의 기본 필터 단계들로 구성되어 있어서, 사실상 실제 처리 단계 수는 20개 이상일 수 있다.

필터 인터페이스가 슬라이더 기반이기 때문에 필터링 성능이 매우 중요하다
[그림 2] 필터 인터페이스가 슬라이더 기반이기 때문에 필터링 성능이 매우 중요하다.

슬라이더 기반의 효과 인터페이스의 필터링 성능은 사용자 경험(user experience)에 큰 영향을 미친다. 따라서 셔터스톡은 초당 60프레임으로 필터링을 실행하기 위해 노력하고 있다. 초당 60프레임으로 나눈 1000밀리초(ms)는 전체 이미지 필터당 16ms의 렌더링 버짓(budget)을 산출한다. 그 버짓을 20개의 필터 렌더링 단계로 나누면 한 단계당 1ms 미만이 남는다.

사용자들의 진행속도가 자바스크립트보다 더 빠르기 때문에, 셔터스톡은 최대한 GPU 프로세싱을 우선적으로 사용하여 성과를 거두고, 그 외 CPU만 사용하는 경우에도 최선을 다하고 있다.

- CPU 필터링 활용

필터링 프로세스에 대해 알아가기 위해 CPU만 사용해 이미지 필터링을 하는 방법에 대해 살펴보자. 셔터스톡 에디터는 이미지를 HTML 캔버스 요소에 보관하는데, 이는 특정 이미지 데이터를 getImageData 렌더링 방법을 통해 가져올 수 있다는 뜻이다. 이 데이터는 픽셀의 1차원 배열 형태를 가지고 있으며, 각각의 픽셀은 적색, 녹색, 청색, 알파 채널을 나타내는 4개의 연속 정수로 저장된다. 따라서 CPU를 사용하여 필터를 적용하려면 다음을 수행해야 한다.

  • getImageData를 사용하여 이미지 데이터를 픽셀 배열로 복사한다.
  • 각 픽셀에 필터 알고리즘을 적용해 한 번에 한 픽셀씩 배열을 반복한다. 간단한 필터는 각 색상 채널에 상수를 추가할 수 있다. 이미지를 그레이스케일(grayscale)로 변환하는 것은 모든 컬러를 평균화시키고 각 컬러 채널에 동일한 값을 적용하는 방법이다.
  • 추가되는 필터링 단계들도 배열을 다시 반복한다.
  • putImageData 방법을 사용해 결과 배열을 HTML 캔버스에 다시 배치

최신형 컴퓨터에서 자바스크립트의 CPU를 사용한 필터링은 표준 1500px 이미지에서 필터당 40~120ms의 시간이 소요된다. 이러한 필터들이 메인 인터페이스 스레드에서 빠르게 쌓인다면 필터링 작동을 차단할 수 있다. 따라서 CPU를 사용하는 것이 나쁘지 않은 방법임에도 불구하고, 셔터스톡은 가능하면 GPU를 사용하는 방법을 선호하고 있다.

- GPU 필터링 활용

셔터스톡이 HTML Canvas 요소에 보관한 데이터는 GPU에서 WebGL과 셰이더(shader)를 통해 처리될 수 있다. GPU를 사용하여 필터링하는 방법은 다음과 같다.

  • GPU 메모리에 저장할 이미지의 텍스처 객체를 생성한다.
  • 필터를 처음 사용하는 경우, GPU와 호환되는 2진 포맷으로 코드를 번역한다.
  • 번역된 프로그램을 GPU를 통해 텍스처상에서 실행하고 픽셀을 병렬 처리한다.
  • 위 제시된 CPU 프로세싱과 같은 방법으로 한 필터에 하나의 프로그램을 실행한다.
  • 결과 텍스처를 HTML 캔버스에 다시 배치한다.

이 과정은 CPU 필터링과 유사하지만, 훨씬 빠르게 진행된다. 위에서 언급한 최신형 컴퓨터에서 하나의 필터로 이미지를 필터링하면 0.2~0.4ms 정도의 시간이 소요된다. 이 시간의 상당 부분은 DOM에서 GPU로 데이터를 복사하고 다시 가져오는 데 쓰이기 때문에, 연속적으로 다양한 필터들이 적용된 이미지를 60fps 렌더링을 사용하지 않고도 수행할 수 있다.

WebGL의 단점

물론 WebGL에는 몇 가지 단점들이 있다.

  • WebGL 실행은 브라우저, 운영체제 하드웨어 드라이버와 하드웨어에 따라 다르다.
  • 오류 처리 방식에 한계가 있다.
  • 학습 곡선 - WebGL 프로그래밍은 작업 세트가 좁으며, 그 용어로 생각하는 법을 배워야 한다.
  • 텍스처 크기가 제한돼 있어 큰 이미지를 필터링하려면 다른 처리 방식이 필요하다.

WebGL을 사용할 수 없거나, 사용 사례를 지원할 수 없거나, 치명적인 오류를 발생시키는 경우, 어쩔 수 없이 다시 CPU 필터링을 사용해야 한다.

- WebGL 필터링 작동 방식

아래 나와 있는 두 사진들 중, 위쪽에 위치한 이미지를 아래쪽에 위치한 이미지로 만들고자 하는 사용자를 예로 들어보자. 이러한 경우에는 아래 그림에 나온 필터 배열과 같이 사용자가 원하는 결과를 얻을 때까지 필터 패널 조절이 필요하다.

이 출력을 얻기 위해 원본 이미지는 텍스처로 GPU에 업로드되며, 우리는 그것을 originalTexture라고 부른다. 이미지와 동일한 크기로 두 개의 다른 빈 텍스처(textureA, textureB)가 생성된다. 그런 다음, 이미지를 하나의 필터 프로그램에서 다음 필터 프로그램으로 처리하고, 전체 필터 체인이 실행될 때까지 textureA와 textureB사이에서 출력을 주고받는다.

최종 필터가 실행되면, 우리는 textureB의 출력을 HTML 캔버스에 기록한다. 체인에 있는 필터 중 하나라도 수정되는 경우에는 originalTexture부터 시작해 모든 프로세스를 반복해야 한다. 성능 향상(필터 체인 상단에서 각 렌더를 시작하는 비용)을 위해 저장 공간(저장할 중간 텍스처의 수)을 교환함으로써 중간 처리 단계를 유지할 수 있지만, GPU 프로세싱의 경우 필터링 성능은 거의 문제가 되지 않는다.

- 큰 이미지 필터링

셔터스톡의 이미지 크기는 대부분 8000px × 5000px보다 크며, 사용자 콘텐츠가 이보다 더 클 수도 있다. 셔터스톡 에디터를 빠르게 유지하기 위해 대부분의 경우 미리보기 이미지를 사용한다. 사용자가 최종 렌더링을 수행할 때에만 고해상도 이미지를 필터링할 필요가 있다. 그럼에도 불구하고, 필터링 단계는 다른 것과 마찬가지로 중요하다. 이렇게 큰 이미지를 처리하려면 다음과 같은 제약 조건 내에서 작업해야 한다.

브라우저에 따라 최대 캔버스 크기는 5000px~1만2000px이다.

하드웨어에 따라 최대 WebGL texture 크기는 2048px에서 1만6384px까지 다양하다.

따라서 큰 이미지의 경우 셔터스톡 에디터는 이미지를 각각 크기가 2048px × 2048px인 타일로 분할해 한 번에 4메가픽셀을 필터링한다. 이 크기는 대부분의 하드웨어와 모든 주요 브라우저에서 작동한다. 대부분의 미리보기 이미지가 하나의 타일 안에 들어갈 수 있도록 허용하는데, 이는 전체 크기의 이미지를 사용하여 최종 렌더링을 수행할 때를 제외하고 기본적으로 이 타일로 우회하는 것을 의미한다.

- 타일을 사용해 필터링하는 방법

그렇다면 타일을 사용해 이미지를 필터링하려면 어떻게 해야 할까? 첫 번째 생각은 다음과 같을지도 모른다.

커다란 이미지가 일련의 인접한 타일들로 분할돼 있는데, 각각 분할된 부분들은 WebGL 텍스처 안에 충분히 들어갈 정도로 작다. 그러나 이러한 설정은 블러와 같은 필터 때문에 셔터스톡 에디터에서는 실행되지 않는다.

블러와 같은 필터를 실행하기 위해서는 주변 픽셀들을 인식해야 한다. 위에 그려진 타일의 가장자리 부분에는 주변 픽셀이 빠져 있는 것을 볼 수 있다. WebGL 또는 2D 도면의 통합된 논리는 픽셀을 가장자리 부분부터 반영해 독립된 이미지에 블러 효과를 만들 수 있지만, 이러한 타일들을 다시 함께 붙이면 두 개의 인접한 타일의 가장자리에 있는 블러 처리된 부분이 서로 연결되지 않는다.

[그림 3] 이웃한 두 타일의 블러 처리된 부분이 서로 연결되지 않아 가장자리 부분에 주름이 남은 모습
[그림 3] 이웃한 두 타일의 블러 처리된 부분이 서로 연결되지 않아 가장자리 부분에 주름이 남은 모습

따라서 블러 효과를 적용하기 위해서는 타일을 서로 겹치게 하는 것이 필요하다. 이를 위한 설정은 다음과 같다.

[그림 4] 이미지를 6개의 타일로 분할(타일이 겹쳐져 공백이 생기지 않는다)
[그림 4] 이미지를 6개의 타일로 분할(타일이 겹쳐져 공백이 생기지 않는다)

이미지는 두 가지 중요한 규칙에 따라 6개의 타일로 분할된다. 첫째, 타일들이 서로 만나는 곳에는 필터가 요구하는 크기의 2배에 해당하는 겹침이 있어야 한다. 예를 들어 블러 필터의 반지름이 10픽셀인 경우에는 두 타일 사이의 겹치는 부분이 최소 20픽셀 이상이 돼야 한다.

둘째, 다른 타일들과 겹쳐지지 않는 타일의 모든 가장자리 부분은 이미지의 가장자리에서 끝나야 한다. 이 규칙을 통해 모든 타일에 빈 공간이 없도록 하여 한 타일당 최대한 단순하게 처리될 수 있도록 유지할 수 있다.

처리 후 타일들은 다시 결합되며, 각 타일은 겹치는 부분을 고려하여 데이터의 절반을 제공한다. 결합의 중심에 있는 두 타일의 경계를 따라 생기는 크리즈 현상을 피하고자 정확히 동일한 데이터를 공유해야 하기 때문에, 겹치는 부분이 필터가 요구하는 크기의 2배가 되는 것이 필수적이다.

결론

이미지를 필터링하는 것은 브라우저에서도 간단하게 작업할 수 있다. 그러나 다양한 사용자들에게 광범위한 수행 필터를 제공하기는 쉽지 않은 작업이다. 셔터스톡 에디터는 자바스크립트와 WebGL 양쪽에서 평행한 필터 실행을 유지하고, GPU 텍스처를 최대한 재사용하며, 큰 이미지를 하드웨어와 소프트웨어 조합이 관리할 수 있는 중복 타일들로 분할함으로써 어려움을 극복하고 뛰어난 성능의 이미지 필터링 기능을 제공하고 있다. 또한, 해당 기능들을 많은 고객이 활용할 수 있도록 다양한 언어로 지원하고 있다.



댓글삭제
삭제한 댓글은 다시 복구할 수 없습니다.
그래도 삭제하시겠습니까?
댓글 0
댓글쓰기
계정을 선택하시면 로그인·계정인증을 통해
댓글을 남기실 수 있습니다.