영화로부터 잘 알려진 것처럼 애니메이션(Animation)은 어떤 사물의 움직임 내지 장면의 변화를 표현하는 기술이다. 특히 애니메이션 기술은 만화나 영화, 게임 등 여러 영상 콘텐츠에서 자연스러운 동작 표현을 위해 사용되었다. 이러한 애니메이션은 UI에서도 예외가 아니다. UI 시스템은 애니메이션을 통해 콘텐츠뿐만 아니라 UI 객체의 상태 전이를 시각적으로 부각할 수 있다. 이는 사용자의 시선을 문맥의 흐름에 집중시킴으로써 사용자 이해를 높이고 앱의 사용성을 개선하는 데 도움을 준다. 무엇보다도 애니메이션을 통해 UI 앱의 콘텐츠를 보다 생동감 있게 표현함으로써 사용자 이목을 집중시키는 한편 앱의 가치를 높일 수 있다.


한편, UI 시스템에서 비주얼 인터렉션(Visual Interaction)은 애니메이션과 동일한 범주 내에서 해석할 수 있다. UI 앱은 사용자 입력 내지 제스처로부터 데이터를 가공한 후 모션 그래픽 효과를 애니메이션과 함께 표현할 수 있다. 이러한 애니메이션 효과는 장면마다 연속된 이미지를 화면에 출력함으로써 개체가 마치 어떤 동작을 수행하는 것처럼 표현한다.


본 장에서는 UI 시스템에서 애니메이션과 함께 비주얼 인터렉션을 구현하기 위한 주요 기술을 구체적으로 살펴본다. 비주얼 인터렉션은 오늘날 UI의 꽃이라고 해도 과언이 아니며 고급 UI를 위한 필수 요소에 해당하므로 사례와 함께 구현 메커니즘을 깊이 있게 학습해 보도록 한다.



1. 학습 목표

이번 장을 통해 다음 사항을 학습해 보자.

  • 프레임 기반 애니메이션 재생 원리를 이해한다.
  • FPS 개념을 이해하고 엔진에서 이를 결정하는 방법을 살펴본다.
  • 프로퍼티 기반 애니메이션 구현 방법을 살펴본다.
  • 커스텀 가능한 사용자 애니메이션 제공 방안을 살펴본다.
  • 애니메이션 시간을 조작하고 애니메이션 가속 제어 방식을 이해한다.
  • 애니메이션을 활용한 이미지 필터 효과를 구현한다.
  • 비주얼 인터렉션을 위한 제스처 이벤트 생성 과정을 살펴본다.
  • 프레임 저하를 방지하기 위해  스케줄링 기반의 멀티 태스킹 방법을 살펴본다.


  • 2. 애니메이션 기초

    이번 절은 6.3절 실용 애니메이션 기술의 기반이 되는 기술 사항으로서 UI 엔진 구축에 중요한 사항에 해당한다. 본 절에서는 UI 시스템에서 필요한 애니메이션 필수 기반 요소에 대해서 살펴본다. 프레임 애니메이션을 통해 애니메이션 동작 원리를 학습하고 그 과정에서 애니메이션 루프, FPS, 시간 개념 등 애니메이션 주요 개념을 이해한다.


    2.1 프레임 애니메이션

    프레임(Frame) 애니메이션의 원리는 기록한 장면을 순차대로 출력한다. 전통 애니메이션 영화를 떠올리면 쉽게 이해할 수 있다. 일반적으로 프레임 애니메이션은 장면 이미지를 디자인 단계에 미리 가공하므로 앱에서 필요한 리소스는 크지만 준비한 이미지를 시간 순서대로 출력하면 되므로 구현은 단순하다.


    프레임 애니메이션 동작 이해를 위한 간단한 예시로서 그림 6.1과 같이 연속된 장면의 이미지를 시간 순서대로 화면에 출력한다.



    그림 1: 연속된 장면 출력 (Calvin and Hobbes)

    요약하자면, 프레임 애니메이션을 구현하기 위해서는 다음 두 사항을 고려해야 한다.

  • (연속된) 장면 이미지 준비
  • 준비된 장면을 시간 순서대로 화면에 출력

  • 이를 코드로 옮기면 다음과 같다.

    /* * 10개의 장면으로 구성된 애니메이션을 구현한다. * 본 예제에서는 이미디어트 렌더링 방식을 이용한다. */ frameAnimation(): //장면 1 준비 img1 = UIImage(): .path = “frame0.jpg” .geometry = {x, y, w, h} //첫 번째 장면을 그리기 위해 무효 영역을 설정 invalidateArea(x, y, w, h) //화면 갱신 요청 drawScreen() //장면 2 준비 img2 = UIImage(): .path = “frame1.jpg” .geometry = {x, y, w, h} //두 번째 장면을 그리기 위해 무효 영역을 다시 설정 invalidateArea(x, y, w, h) //화면 갱신 요청 drawScreen() //동일하게 열 번째 장면까지 반복 작업을 수행

    ...

    코드 1: 프레임 애니메이션 구현

    코드 1에서는 순차적으로 장면 이미지를 준비하고 화면 출력 요청을 한다. 다만 코드 1에서는 프레임을 고려하지 않고 하나의 루틴에서 모든 장면을 연속으로 출력하고 있기 때문에 이전 장면이 화면에 보일 새도 없이 마지막 장면이 화면에 나타날 것이다. 일반적으로 초당 30~60장면을 출력하면 부드러운 애니메이션을 표현할 수 있는데 이와 관련된 자세한 사항은 2.2절에서 살펴본다.

    • 애니메이션 루프
    드로잉을 직접 요청하지 않는 리테인드 모드의 그래픽스 시스템에서는 UI 엔진이 프레임마다 UI 앱에게 장면을 준비하도록 요청한다. 이는 UI 엔진이 메인루프를 통해 프레임 변화를 감지하고 새로운 장면을 출력해야 하는 시점을 직접 통제하기 때문에 가능하다. 메인루프의 사이클마다 화면 갱신을 수행한다면 (그림 2) 매 사이클을 1프레임으로 간주할 수 있다. 이제 UI 엔진은 화면 갱신 전 애니메이션 이벤트를 발동하고 앱은 그 이벤트를 받아 애니메이션 장면을 변경할 수 있다. 이 절차는 애니메이션을 종료할 때까지 반복하며 우리는 이를 애니메이션 루프라고 명명한다.

    /* * 10장의 장면으로 구성된 애니메이션을 구현한다. * 본 예제에서는 리테인드 렌더링 방식을 이용한다. * 앱은 애니메이션 콜백 함수를 등록하고 콜백 함수에서 장면을 구축한다. */ frameAnimation(): /* 애니메이션 객체를 하나 생성한다. UIAnimation은 UIEngine으로부터 이벤트 신호를 받고 프레임마다 f()를 호출한다.*/ animation = UIAnimation()

    /* 애니메이션 콜백 함수를 클로져(Closure) 방식으로 표현.

    현재 프레임에 해당하는 장면을 준비한다.

    img 객체는 미리 초기화되어 있다고 가정한다. */

    f = closure():

    //새로운 장면 이미지로 교체

    img.path = "frame" + frame + ".jpg" //path = frame0.jpg

    ++frame; //다음 프레임 번호 결정 (frame1)


    animation.EventUpdated += {f, ...}

    //애니메이션 가동 animation.play()

    코드 2: 이벤트 리스너 방식의 애니메이션 구현

    그림 2: 애니메이션 루프 수행 과정


    사용자 애니메이션과 메인루프를 연결함으로써 애니메이션 루프를 완성한다. 이때 메인루프와 UIAnimation을 연결하고 애니메이션을 일괄 중재하는 UIAnimationCore 클래스를 도입하여 구조를 체계화한다. UIAnimationCore는 생성된 UIAnimation 인스턴스를 리스트로 관리한다. UIEngine은 메인루프의 이벤트 처리 단계에서 UIAnimationCore를 통해 UIAnimation 갱신 작업(update)을 일괄 수행한다. 이때 UIAnimation은 사용자 콜백 함수 f()를 호출하여 사용자 애니메이션을 수행한다.

    //UIAnimation은 UIAnimationCore에 자신의 인스턴스를 등록한다. UIAnimation.constructor(): UIAnimationCore.register(self)

    코드 3: UIAnimationCore와 UIAnimation의 연동

    • GIF 애니메이션
    4장에서 소개한 GIF(Graphics Interchange Format)는 프레임 애니메이션의 대표 예다. GIF는 애니메이션을 구성하는 연속된 장면 이미지를 하나의 데이터로 압축하여 기록한다. 앱으로부터 GIF 출력 요청을 받은 UI 엔진은 앞서 살펴본 애니메이션 처리 루틴을 거쳐 GIF로부터 출력해야 할 현재 프레임 번호를 결정한다. 이후 프레임 번호를 렌더링 엔진에 전달하면 렌더링 엔진은 GIF 디코딩 과정을 거쳐 GIF 데이터로부터 해당 프레임의 비트맵 이미지를 생성할 수 있다. 마지막으로 생성한 장면 이미지는 지정된 이미지 렌더링 절차를 거쳐 화면에 출력한다. UI 엔진이 가동하는 동안 애니메이션 루틴은 메인루프를 통해 재차 수행되므로 프레임 번호를 갱신할 수 있고 최종적으로 애니메이션이 동작하는 결과를 보여줄 수 있다.

    //UIAnimatedImage는 애니메이션 재생 가능한 이미지 기능을 담당한다. img = UIAnimatedImage(): .path = “sample.gif” //GIF 파일을 불러온다. .geometry = {x, y, w, h} .play() //애니메이션 재생

    코드 4: GIF 애니메이션 구현

    /* * 이미지 애니메이션 재생 */ UIAnimatedImage.play():

    .animation = UIAnimation() //애니메이션 생성


    /* 애니메이션 구현 함수를 클로져 형식으로 표현 animation은 UIAnimation 자신을 가리키고 target은 호출한 UIAnimationImage 객체를 가리킨다. */ f = closure(UIAnimation animation, UIAnimatedImage target):

    //다음 프레임 번호 결정

    ++target.frame //프레임 번호가 끝에 도달하면 애니메이션 종료 if(target.frame == target.maxFrame) animation.finish()

    .animation.EventUpdated += {f, self} .animation.play()

    코드 5: GIF 애니메이션 재생부

    코드 5는 UIAnimatedImage을 이용하여 프레임 애니메이션을 재생하는 주요 로직을 보여준다. UIAnimatedImage는 애니메이션을 재생하기 위해 UIAnimation 기능을 내부적으로 활용한다. 
    애니메이션 콜백 함수 내에서는 프레임 번호를 증가하는 작업을 수행한다. 만약 프레임 번호가 마지막에 도달한다면 애니메이션을 중단시킨다. 여기서 UIAnimatedImage는 frame() 메서드를 통해 출력할 애니메이션 프레임 번호를 전달받는데 그 내부에서는 UI 렌더링 엔진을 통해 GIF 데이터로부터 해당 프레임의 이미지를 디코딩하여 화면에 출력할 수 있다고 전제한다. (그림 4.4 참고)

    앞선 과정을 통해 연속된 장면 이미지를 출력하는 방법을 이해했지만 실제로 애니메이션은 지정된 시간에 맞춰 모든 장면을 출력해야 한다. 이를 위해 우리는 FPS의 개념을 먼저 이해해야 한다.


    2.2 프레임 제어

    • FPS (Frames Per Second)

    FPS는 프레임률(Frame Rate)라고도 하며 초당 화면 출력 수를 의미한다. 따라서 FPS가 높을수록 부드러운 애니메이션 표현이 가능하다. 다만 현대의 콘텐츠에서 애니메이션은 60fps를 지향하는데 프레임률이 60을 초과하더라도 시청자는 그 차이를 인지하기 어렵기 때문이다. 오히려 과도한 프레임률로 인해 프로세싱 부하와 전력 소모가 증가한다. 반대로 시청자는 60fps 미만에서 애니메이션이 부드럽지 않다는 느낌을 받을 수 있고 30fps 미만이라면 그 차이를 쉽게 감지할 수 있다.


    이런 이유로 산업에서는 출력 장치 화면 주사율을 60Hz에 맞게 설계한다. 이 수치는 일부 게이밍 또는 3D 모니터를 제외하고 현재 상용 제품의 표준 수치로서 통용된다. 따라서 소프트웨어 역시 디바이스 성능을 바탕으로 60fps 동작을 지원해야 한다.


    이제 실제 동작 예시를 들어보자. 60fps의 3초짜리 애니메이션을 출력한다면 총 180장면의 이미지를 생성해야 한다. 이는 UIEngine.run()에 의해 수행되는 메인루프의 반복 횟수가 총 180번임을 의미하며 초당 60번을 반복해야 한다. 달리 말하면 메인루프는 1초당 60번의 화면 갱신(렌더링) 작업을 수행해야 한다.


    그림 3: 고정 60fps 메인루프 렌더링 수행 과정

    따라서 60fps를 보장하기 위해서는 애니메이션 장면을 준비하고 출력하는 과정을 약 0.0167초 이내에 완성해야 한다. 만약 메인루프의 한 사이클을 수행하는 데 걸리는 시간이 이보다 적다면 60fps를 초과할 수도 있다. 이 경우엔 60fps를 초과하지 않도록 메인루프 동작을 지연시켜야 한다. 반대로 메인루프 한 사이클을 수행하는데  0.0167초를 초과한다면 60fps를 보장할 수 없다. 이 경우 우리가 고려할 수 있는 해결책은 소프트웨어를 최적화하거나 고성능의 하드웨어 장비로 교체하는 것이다.

    • 수직 동기화

    시스템이 수직 동기화(vsync)를 요구한다면 시간 엄수는 더 엄격하다. 특히 멀티 윈도우 환경이라면 디스플레이 장치는 여러 클라이언트(앱)가 공유하는 자원에 해당하므로 한 클라이언트만을 위해 화면 갱신(refresh) 작업을 수행할 수 없다. 이러한 구조에서는 윈도우 서버/컴퍼지터가 초당 60회로 클라이언트(UI 앱)의 화면을 합성하여 출력한다. 이는 마치 시간을 엄수하며 출발하는 기차와도 같다. 따라서 컴퍼지터가 화면 갱신 작업을 수행하기 전에 클라언트가 화면 출력을 완수하지 못한다면 윈도우 서버의 다음 프레임 출력까지 클라이언트는 출력 결과물을 화면에 내보낼 수 없게 된다. 따라서 이 경우 클라이언트의 출력 장면은 다음 프레임으로 미루거나 철회한다.



     스크린 티어링 (Screen Tearing)


    스크린 티어링(화면 찢김 현상)은 한 화면에 여러 프레임의 이미지가 동시에 기록되는 현상이다. 이는 출력 버퍼에 렌더링 장면을 기록하는 도중에 출력 버퍼 장면을 화면상으로 내보내어 발생하며 마치 화면이 찢어지는 현상처럼 보인다.


    이를 방지하기 위해 다중 버퍼를 이용하거나 화면 주사가 완전히 끝날 때까지 출력 버퍼 기록 작업을 대기하여 동기화 작업을 수행한다.


    • 더블 / 트리플 버퍼링

    한편 클라이언트가 렌더링 결과물을 출력 버퍼에 기록하는 동안 윈도우 서버가 클라이언트의 출력 버퍼에 접근하기 위해 이중 또는 삼중 출력 버퍼를 이용할 수 있다. 이를 더블(Double) 또는 삼중(Triple) 버퍼링이라고 한다. 이러한 기법을 이용하면 스크린 티어링 현상을 방지하고 클라이언트는 앞서 말한 지연된 프레임을 버퍼에 보존한 채 다음 프레임 장면을 구축할 수 있다. 클라이언트의 화면 출력 작업이 지연될 경우 윈도우 서버는 클라이언트의 주 버퍼(Primary Buffer)에 저장된 이전 화면을 그대로 활용한다.



    그림 4: 더블 버퍼링 출력 시스템 동작 시퀀스


    2.3 시간 측정
    • 타임스탬프 (Timestamp)
    메인루프를 60fps를 목표로 가동하기 위해선 0.0167초 내에 한 프레임을 완수해야 한다. 이를 위해 시간 측정은 필수이다. 범용 운영체제라면 운영체제에서 제공하는 타임스탬프(Timestamp) 기능을 활용하여 경과 시간을 계산할 수 있다.

  • 리눅스(유닉스 계열): clock_gettime()
  • 윈도우즈: timeGetTime()
  • : mach_absolute_time()

  • /* * Linux clock_gettime()을 이용한 타임스탬프 확인 * UIEngine은 시간을 확인하기 위해 time()을 호출한다고 가정

    * 코드는 C 언어로 작성 */ #include <time.h> double time() { struct timespec timestamp; double seconds; //실시간 시간 정보를 timestamp에 기록 clock_gettime(CLOCK_REALTIME, ×tamp); //timestamp에 기록된 시간을 초 단위로 환산 //Nanoseconds -> Microseconds -> Milliseconds -> Seconds seconds = timestamp.tv_sec + (timestamp.tv_nsec / 1000000000L); return seconds; }

    코드 6: 타임스탬프 사용 예 (리눅스)

    타임스탬프의 시간을 이용하여 이전 사이클 시간과 현재 사이클 시간을 각각 기록하고 두 시간의 차, 즉 경과 시간(Elapsed Time)을 통해 한 프레임에 걸린 시간을 확인한다. 만약 경과 시간이 0.0167초보다 적다면 남은 시간만큼 대기(idle) 또는 수면(sleep) 상태로 메인루프의 동작을 지연시킴으로써 한 프레임을 완수하는 데 걸리는 시간을 조율한다.

    그림 5: 프레임 경과 시간 계산


    반대로 0.0167초를 초과한 경우 윈도우 서버는 클라이언트 출력물을 화면에 반영 못 할 가능성이 높기 때문에 해당 프레임은 누락되었거나 지연되었다고 해석할 수 있다. 이때 더블 또는 트리플 버퍼링을 적용한 경우 백버퍼(Secondary Buffer)에 출력물을 기록한 채 시간이 지연된 만큼 바로 다음 프레임을 수행한다.

     CPU Scaling Governor

    정확한 FPS를 측정하기 위해 CPU 주파수(Frequency)를 조율하는 방법을 배워두자. 리눅스에서는 CPU 주파수를 실시간 조절함으로써 프로세서 파워를 제어한다. 본 정책을 CPU Scaling Governor라고 하는데 하드웨어 성능에 민감한 임베디드 시스템에서는 CPU 주파수를 조절함으로써 프로세서 파워를 낮추고 전력 소모를 최소화한다. 문제는 이 정책으로 인해 성능 측정 중 프로세서 파워가 바뀔 수 있으며 이는 성능 측정 결과에도 적지 않은 영향을 미칠 수 있다. FPS를 통해 성능 비교를 할 경우 측정 환경은 동일해야 하므로 성능 측정 시 프로세서 파워를 고정하는 작업이 필요하다.

    리눅스에서는 CPU 주파수 제어 정책을 정의한다. 다음 명령어를 통해 현 시스템에서 설정 가능한 정책을 열람한다.

    $cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_available_governors

    performance, powersave, userspace, schedutil
    이 중 가장 높은 성능을 내도록 performance 정책을 이용한다.

    $echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
    다른 방법으로는 CPU 주파수 값을 직접 명시할 수 있다. 지정된 CPU 주파수의 최소, 최댓값을 확인하기 위해서 다음 명령어를 이용한다.
    $cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_min_freq

    1200000

    $cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq

    3300000

    현 시스템에서 가용한 CPU 주파수 목록을 확인한다.

    $cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_available_frequencies 1200000 2400000 3300000

    프로세서를 최대 파워로 동작하기 위해 scaling_min_freq와 scaling_max_freq를 최대 수치(3300000)로 지정한다.

    $echo 3300000 > /sys/devices/system/cpu/cpu*/cpufreq/scaling_min_freq

    $echo 3300000 > /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq

    cpu*의 *은 cpu 번호로 대처한다. 예로, 8코어는 cpu0, cpu1, cpu2, cpu3 … cpu7. 멀티 코어를 활용한다면 모든 CPU를 대상으로 같은 작업을 수행한다.

    더욱 자세한 사항은 다음 문서를 참조하자.

    https://www.kernel.org/doc/html/v4.14/admin-guide/pm/cpufreq.html


    2.4 시간 제어

    코드 5 GIF 애니메이션 재생부를 확장하여 시간 제어 로직을 추가한다. 코드 5는 프레임(Frame) 애니메이션을 구현하는데 지정된 시간 동안 애니메이션을 재생하기 위해서는 시간 제어가 필요하다. 예를 들어 애니메이션 재생 시간을 3초로 가정한다면 UIAnimation의 수행 로직인 f()는 60fps 수행 환경에서 3초 동안 180번 호출된다.


    일단 UIAnimation 내부 구현은 무시한 채 UIAnimation()이 수행 시간을 추가 인자로 받고 콜백 함수에서 애니메이션 재생 흐름(progress)을 인자로 전달한다고 가정한다.


    UIAnimatedImage.play(): /* UIAnimation()에 전달한 duration을 통해 애니메이션 수행 시간을 지정한다.

    duration은 3초라고 가정한다. */ .animation = UIAnimation(.duration)


    /* progress는 0 ~ 1사이 정규값이다. 0은 애니메이션 시작 시점, 1은 애니메이션 종료 시점에 해당한다. */

    f = closure(UIAnimation animation, UIAnimatedImage target, Var progress): target.frame = target.maxFrame * progress //다음 프레임 번호 결정

    /* 이제 UIAnimation은 애니메이션을 스스로 종료할 수 있음으로 다음 호출은 필요 없다. */

    //if(target.frame == target.maxFrame) animation.finish()


    .animation.EventUpdated += {f, self}

    /* play()는 루프타임을 이용하여 시작 시간을 기록한다고 가정한다. */ .animation.play()

    코드 7: 지정 시간 내 애니메이션 재생

    코드 7 다섯 번째 줄에서 UIAnimation을 생성하면서 수행 시간을 명시한다. 여기서 UIAnimatedImage는 GIF 데이터로부터 애니메이션 재생 시간 정보를 얻고 UIAnimation에 전달한다고 가정한다. 이제 UIAnimation은 play() 호출 후 3초간 사용자 콜백 함수 f()를 호출한다. 이때 경과 시간을 정규값 progress로 전달함으로써 사용자가 애니메이션의 어느 구간에 위치했는지 확인할 수 있게 한다. 따라서 progress 0은 애니메이션의 시작, 1은 끝에 해당한다. 이제 UIAnimation은 애니메이션 종료 시점을 직접 결정할 수 있음으로 사용자가 animation.finish()를 호출하지 않아도 자동 종료를 수행할 수 있다. (열네 번째 줄) UIAnimatedImage는 progress의 값을 GIF 프레임 번호에 매핑하여 출력할 장면을 결정한다. (아홉 번째 줄)

    콜백 함수 f()를 호출하는 과정을 살펴보면, UIEngine은 메인루프 이벤트 처리 단계에서 UIAnimationCore를 통해 UIAnimation을 갱신(update)한다. (그림 2) 이때 UIAnimationCore는 루프 타임을 구하고 이를 UIAnimation으로 전달한다. UIAnimation은 전달받은 루프 타임에서 각자의 애니메이션 시작 시각을 빼서 경과 시간을 구한다. 그리고 이를 정규값으로 변환 후 콜백 함수의 progress 인자로 전달한다.

    UIAnimationCore.update(): /* 루프 타임 갱신 */ .loopTime = UITime.loopTime() /* UIAnimation 갱신 */

    foreach(.animations, animation)


    animation.update(.loopTime)


    /* 갱신 후 애니메이션이 종료된 경우 리스트에서 제거 */

    if(animation.invalid()) .animations.remove(animation)

    코드 8: UIAnimation 갱신

    UIAnimation.update(current): /* progress 값을 구한다. begin은 play() 수행 시 기록한 시작 시각이며

    duration은 애니메이션 재생 시간이다. */ progress = (current - .begin) / .duration /* 시간 초과에 주의 */

    if(progress > 1) progress = 1


    /* 콜백 함수를 호출한다. 마지막 인자로 progress를 전달하는 것에 주목 */

    .userFunc(self, .target, progress)


    /* 애니메이션 종료 */

    if(progress == 1) .finish()

    코드 9: UIAnimation progress 계산 로직


    2.5 가속 제어
    • 이징 애니메이션 (Easing Animation)

    UI 프레임워크는 애니메이션의 심미적 효과를 높일 수 있도록 이징(Easing)을 정의한다. 이징은 애니메이션의 속도 개념으로 볼 수 있으며 타입에 따라 UIAnimation의 progress 값에 변화를 준다. 방법에 따라 이징 애니메이션은 다음과 같은 속도 곡선을 보인다.


    그림 6: Easing Animation 곡선 예시


    • 인터폴레이터 (Interpolator)
    이징 애니메이션에 변화를 주기 위해서 인터폴레이터(Interpolator)을 정의할 수 있다. 인터폴레이터는 외부에서 progress 값을 조정하는 수단이므로 이를 이용하면 애니메이션 장면 변이를 바꿀 수 있다. 가령 다섯 프레임에 걸쳐 애니메이션을 수행한다고 가정해 보자. progress는 [0 0.25, 0.5, 0.75, 1]과 같은 선형에 수렴한 값으로서 전달될 것이다. 이때 인터폴레이터가 개입하여 progress 값을 [0 0.125, 0.2, 0.5, 1]로 바꿀 수 있으며 결과적으로 이는 장면 변화 속도에 영향을 미친다. 심지어 progress 값을 잘 조율한다면 되감기(Rewind) 또는 반동(Bounce)과 같은 효과를 주는 것도 가능하다.

    Rewind [1 0.75 0.5 0.25 0]

    Bounce [0.125 0.2 0.4 0.8 1.125 1.2 1.125 1.0]

    Rewind와 Bounce Progress 예시


    인터폴레이터(UIInterpolator) 구현 핵심은 출력하고자 하는 곡선 그래프를 선정한다. 등속에 해당하는 선형 값(progress’)을 입력값으로 이용한다. 입력값으로부터 선정한 곡선 그래프의 수식을 이용하여 출력값(progress)을 산출하고 이를 반환한다. 마지막으로 이 값을 UIAnimator의 콜백 함수 f()에 progress 인수로 전달한다.


    그림 7: UIInterpolator 동작 시퀀스

    다음은 예시로서 Ease Out을 구현한다.

    /*

    * UIInterpolator 인터페이스를 구현한다.

    * 멤버 함수 map()은 인자 progress[0 - 1] 값을 사인 곡선[0 - 90]에 매핑한 값으로 변환한다.

    */ UIEaseOutInterpolator implements UIInterpolator:

    override map(progress):

    return sin(progress * 90)

    코드 10: Ease Out Interpolator 구현부

    코드 10에서 확인할 수 있지만, 인터폴레이터 기능을 인터페이스로 제공하여 사용자가 원하는 인터폴레이터를 추가할 수 있게 한다.

    UICustomInterpolator implements UIInterpolator:

    override map(progress):

    //TODO: 원하는 그래프 곡선을 구현하고 progress 값을 변환한다.

    return xxx(progress)

    코드 11: Custom Interpolator 구현부

    UI 프레임워크에서 제공하는 기본 인터폴레이터 또는 사용자 커스텀 인터폴레이터는 UIAnimator와 연동하여 사용할 수 있다. 

    UICustomInterpolator myInterpolator //사용자 커스텀 인터폴레이터

    animation = UIAnimation()

    ...

    animation.interpolator = myInterpolator //인터폴레이터 적용

    animation.play()

    코드 12: 커스텀 Interpolator 적용 예


    3. 애니메이션 기법

    앞 절에서 배운 애니메이션 기초를 토대로 본 절에서는 UI 프레임워크에서 활용할 수 있는 애니메이션 기법에 대해서 설명한다.

    3.1 프로퍼티 애니메이션

    프로퍼티 애니메이션은 UI 오브젝트의 속성(Property)에 변화를 주어 애니메이션 효과를 나타낸다. 일부 프레임워크에서는 이를 트윈(Tween) 애니메이션라고도 한다. 핵심은 변화를 주고자 하는 오브젝트 속성을 결정하고 이 속성을 매 프레임마다 새로 갱신한다. 대표 속성으로는 오브젝트 위치, 크기, 색상, 투명도(Opacity)가 있으며 사실상 프레임워크에서 허용하는 변경 가능한 속성은 모든 대상이 될 수 있다.


    그림 8: 프로퍼티 애니메이션

    예시로 이동 애니메이션을 표현하기 위해 객체 위치 속성을 변경한다. 객체의 시작 위치와 종료 위치 정보를 입력으로 받고 두 위치 사이에서 UIAnimation progress를 이용하여 현재 객체 위치를 구한다.

    /* 현재 프레임의 객체 위치를 구한다.

    prop은 Property 타입으로서 이는 프로퍼티 애니메이션을 위해 정의한 데이터다.

    toPos와 fromPos는 객체 시작 위치와 종료 위치를 가리킨다.

    target은 프로퍼티 애니메이션을 적용할 대상 객체를 가리킨다. */ UIPropertyTransition.play():

    ...

    f = closure(UIAnimation animation, UIObject target, Var progress):

    curPosition = (prop.toPos - prop.fromPos) * progress + prop.fromPos target.position = curPosition ...

    코드 13: 이동 애니메이션 구현부

    색상의 경우도 다르지 않다.

    /* 현재 프레임의 객체 색상을 구한다.

    toColor와 fromColor는 시작 색상과 종료 색상을 가리킨다. */ UIPropertyTransition.play():

    ...

    f = closure(UIAnimation animation, UIObject target, Var progress):

    curColor.r = (prop.toColor.r - prop.fromColor.r) * progress + prop.fromColor.r curColor.g = (prop.toColor.g - prop.fromColor.g) * progress + prop.fromColor.g curColor.b = (prop.toColor.b - prop.fromColor.b) * progress + prop.fromColor.b target.color = curColor ...

    코드 14: 색상 애니메이션 구현부

    코드 13과 14에서 등장하는 UIPropertyTransition은 프로퍼티 애니메이션을 제공하기 위해 정의한 가상의 클래스이다. 이 클래스는 프로퍼티의 전이를 구현하기 위해 프로퍼티 입력 정보(시작과 종료 지점의 위치, 색상 정보)를 prop에 기록한다.

    오브젝트의 일부 속성은 상호 배타적이므로 단독 또는 동시 적용이 가능하다. 이러한 개념은 프로퍼티 애니메이션을 통해 더욱 동적인 효과를 보일 수 있다.


    //위치 curPosition = (prop.toPos - prop.fromPos) * progress + prop.fromPos target.position = curPosition //색상 curColor.r = (prop.toColor.r - prop.fromColor.r) * progress + prop.fromColor.r curColor.g = (prop.toColor.g - prop.fromColor.g) * progress + prop.fromColor.g curColor.b = (prop.toColor.b - prop.fromColor.b) * progress + prop.fromColor.b target.color = curColor //크기 curSize = (prop.toSize - prop.fromSize) * progress + prop.fromSize target.size = curSize //투명도 curOpacity = (prop.toOpacity - prop.fromOpacity) * progress + prop.fromOpacity target.opacity = curOpacity

    코드 15: 프로퍼티 애니메이션 주 구현부


    한편, 프로퍼티 애니메이션을 이용하는 UI 앱은 효과를 주고자 하는 속성값을 지정해야 한다. 속성값은 코드 15의 toPosition, toColor, toSize, toOpacity에 기록된다. 이때 UI 프레임워크는 사용자가 프로퍼티 값을 지정할 수 있도록 편의 인터페이스를 제공해야 한다. 이를 위해  UIPropertyTransition 라는 가상의 클래스를 도입했다. UIPropertyTransition은 UIAnimatedImage와 동일하게 UIAnimation을 has-a 관계로 구축하여 애니메이션을 수행한다. 

    //프로퍼티 애니메이션 객체 생성 transition = UIPropertyTransition():

    //애니메이션을 수행할 객체 속성 지정 .toPosition = {100, 200} .toScale = 1.5 .toOpacity = 0.5 .toColor = “red”

    //애니메이션 추가 정보 (시간, 반복 횟수, 되감기 여부) .duration = 2.0 .repeat = 3 .rewind = true

    //애니메이션 대상 객체 .target = obj

    //경우에 따라 커스텀 인터폴레이터 적용 (코드 12 참조) myInterpolator = UICustomInterpolator():

    transition.interpolator = myInterpolator

    //애니메이션 재생 transition.play()

    코드 16: UIPropertyTransition 인터페이스 예시


    3. 필터 효과

    본 절에서는 앞 절에서 살펴본 애니메이션 기법 외 애니메이션 응용 사례를 추가로 확인한다. 그중 4.6.3절 이미지 필터는 비주얼 효과를 보여주는 대표 사례로서 여기에 애니메이션을 결합한다면 UI 심미성을 더욱 높일 수 있다. 본 절에서는 4장에서 다루지 않았던 이미지 필터를 구현하고 애니메이션을 적용하는 과정을 학습함으로써 애니메이션에 대한 이해와 응용력을 다진다.



    3.1 블러 필터

    필터 효과의 대표로 꼽을 수 있는 블러는 이미지를 흐리게 하는 효과를 제공한다. 이는 마치 카메라 초점을 조절하는 것과 유사한데 배경 이미지나 특정 뷰를 흐리게 함으로써 일부 UI나 콘텐츠를 강조한다. 실제 iOS의 팝업(Popup) UI는 사용자가 팝업 콘텐츠에 집중할 수 있도록 팝업 배경에 블러 효과를 적용한다.


    그림 9: Modal Popup (iOS)

    • 가우시안 필터 (Gaussian Filter)
    블러 기법으로 Flat, Quadric, Cubic 등 여러 존재하지만, 그 중 가우시안 블러가 좋은 품질을 제공하므로 여기서는 가우시안 블러를 구현한다. 가우시안 분포는 과학 분야의 정규분포 또는 확률분포로써 잡음 제거 목적으로 사용한다. 이차원 영상에서 가우시안 분포를 이용하면 계산하고자 하는 픽셀과 이 픽셀로부터 인접한 이웃 픽셀을 합성한 색상 값을 결정할 수 있다. 이때 거리가 (0, 0)인 픽셀을 표준으로, 표준 편차 값(𝝈)을 결정하면 개입한 이웃 픽셀의 가중치 값을 도출할 수 있다. 표준 편차값이 클수록 영상 흐림 정도가 증가하고 계산량도 그만큼 많아진다.

    그림 10: 표준 편차에 따른 가우시안 곡선


    그림 11: 2차 가우시안 필터 함수

    2차 가우시안 필터 함수(그림 11)에서 x와 y는 구하고자 하는 픽셀 위치로부터의 인접한 픽셀의 거리를 가리킨다. 시그마(𝝈)는 표준 편차로써 이 값이 클수록 개입하는 분포 스펙트럼이 커진다. (그림 10) 따라서 개입하는 픽셀 개수도 많아진다. 이는 블러 효과에서 흐림 단계(power)를 결정하는 변수로 활용할 수 있다.

    구현 핵심을 정리하자면 다음과 같다.

    1. 가우시안 함수를 이용하여 개입할 픽셀의 가중치 값을 테이블(블러 필터 커널)에 기록한다. 
    2. 원본 영상의 픽셀을 순회하며 픽셀마다 커널의 가중치 값을 곱한다.
    3. 2번 단계에 개입하는 인접 픽셀에도 가중치 값을 곱한 후 현재 픽셀과 합산한다.

    UIBlurFilter.power(Var val): kSize = (2 / 2) //커널 테이블 크기 3x3 고정 sigma = val //표준 편차

    .kernel = Kernel[kSize * 2 + 1, kSize * 2 + 1] //중앙 픽셀을 위해 kSize + 1

    /* 가우시안 함수를 이용한 가중치 계산 */ s = 2.0 * sigma * sigma for(y = -kSize; y <= kSize; y++) for (x = -kSize; x <= kSize; x++) r = x * x + y * y weight = exp(-(r / s)) / (PI * s) .kernel[y + kSize][x + kSize] = weight

    코드 17: 블러 필터 커널 가중치 값 계산

    코드 17은 가우시안 필터 함수를 코드로 옮겨놓은 것에서 불과하다. 다만 필터 구현부의 이해를 돕기 위해 커널 테이블을 3x3으로 고정한다. 실제로는 테이블 크기가 더 크거나 가변적이어야 하지만 성능과 품질을 절충한다면 본 예제와 같이 고정할 수 있다. 그리고 고정된 테이블로 인해 손실된 범위 밖의 값을 보정해 줄 필요가 있지만 여기서는 생략한다. 테이블값의 합이 1에 못 미치는 경우 부족한 수치는 각 가중치 값에 분배하여 더할 수 있다. 마지막으로 커널 값은 좌우, 상하 대칭이라는 점을 고려하면 계산 중복을 피하는 것도 가능하다.

    정확한 수치의 가우시안 필터가 아닐지라도 흐림 단계별 커널 테이블을 미리 구축할 수도 있다. 다음 표는 미리 계산한 3x3 크기의 커널 테이블을 보여준다. 이 커널은 좌, 우, 상, 하 아홉 개의 인접 픽셀 가중치 값을 담고 있다.


    커널 테이블을 구축했다면 래스터 단계에서 이를 이용하여 픽셀 색상을 결정한다. 이는 4.6.1절 알파 블렌딩의 색상 혼합 과정과 유사하다. 두 픽셀을 합성하는 것과 동일한 방식으로 인접 픽셀 간 색상을 합성한다. 이때 각 픽셀의 합성 비율은 알파 값이 아닌 커널의 가중치 값으로 결정한다. 코드 18은 핵심 로직 이해를 돕기 위해 3x3 커널 즉, 인접 픽셀에 대해서만 블러 작업을 수행한다. 

    UIBlurFilter implements UIFilter: /* * 블러 필터 * @p in: Pixel * @p coord: Point

    */

    func(in, coord, ...): Pixel out //출력 픽셀 Pixel buffer[,] = .src.map() //블러를 적용할 이미지 Kernel kernel[,] = .kernel //커널 테이블 /* 가중치를 적용하여 인접한 픽셀과 합성 수행 예제는 3x3 커널 크기인 경우로 국한 */ if(.blurSize == 1) //메모리 범주를 벗어나지 않도록 예외처리 하는 코드는 생략 Pixel lt = buffer[coord.y - 1][coord.x - 1] //left top Pixel t = buffer[coord.y - 1][coord.x] //top Pixel rt = buffer[coord.y - 1][coord.x + 1] //right top Pixel l = buffer[coord.y][coord.x - 1] //left Pixel r = buffer[coord.y][coord.x + 1] //right Pixel lb = buffer[coord.y + 1][coord.x - 1] //left bottom Pixel b = buffer[coord.y + 1][coord.x] //bottom Pixel rb = buffer[coord.y + 1][coord.x + 1] //bottom right //픽셀 합성, 가중치를 곱한 픽셀값 합산 out = (lt * kernel[0][0] + t * kernel[0][1] + rt * kernel[0][2] + l * kernel[1][0] + in * kernel[1][1] + r * kernel[1][2] + lb * kernel[2][0] + b * kernel[2][1] + rb * kernel[2][2]) return out

    코드 18: 블러 필터 구현부

    코드 19는 사용자가 블러 단계를 호출하는 예시를 보여준다.

    obj = UIObject(): ... filter = UIBlurFilter(): .power = 1 //블러 단계를 1로 지정 obj.filters += filter //어떤 UI 객체에 블러 필터 적용

    코드 19: 블러 필터 호출부

    이미지 후처리 방식의 필터 작업은 입력 이미지가 선 가공되어야 한다. 코드 18의 아홉 번째 줄에서도 입력 이미지로부터 픽셀 데이터에 접근하는 과정을 보여준다. 이미지 자체에 필터를 적용하는 경우엔 이미지 렌더러를 통해 입력 이미지를 구축할 수 있음으로 이미지를 자연스럽게 입력 데이터로 활용할 수 있다.

    • 렌더 패스 (Render Pass)
    필터 대상이 단일 이미지가 아닌 복합 비주얼 요소로 구성된 경우에는 이미지를 미리 가공할 필요가 있다. 실제로 다수의 필터 효과는 계산 과정에서 대상 이미지 데이터를 필요로 한다. 따라서 이 경우에는 필터를 적용할 UI 객체를 임시 공간(Framebuffer)에 먼저 그린 후 이를 입력 이미지로하여 필터 함수를 수행한다.

    그림 12: 필터 수행을 위한 2단계 렌더링


    그림 12는 2단계 렌더링 패스를 도식화한다. 블러 효과를 적용하기 위해 렌더링 엔진은 두 번의 렌더링 절차를 수행한다. 첫 번째 렌더링은 블러 필터를 적용하는 UI 객체의 원본 이미지를 생성한다. UI 컨트롤과 같이 여러 비주얼 요소를 결합한 상황에 해당한다. 생성한 이미지는 임시 버퍼 내지 프레임 버퍼에 기록할 수 있다. 두 번째 렌더링은 앞에서 생성한 이미지를 입력 데이터로 활용하여 블러 함수를 수행한다. 이미지 렌더링에 대한 구체적인 내용은 4장을 참고한다.

    마지막으로 원본 이미지를 기록한 프레임 버퍼 내지 블러 적용 결과는 4.7.1절에서 설명한 이미지 캐싱 메커니즘을 통해 관리할 수 있으며 이를 통해 비교적 비싼 필터 수행 동작을 최소화할 수 있다.


    3.2 필터 애니메이션

    일정 시간 동안 필터값을 조정하여 필터 애니메이션을 만들 수 있다. 대표로 블러 필터에 애니메이션 적용한다면 시간 경과에 따라 흐려지는 효과가 나타난다.


    그림 13: 블러 애니메이션


    앞에서 애니메이션과 블러 필터 동작을 모두 살펴봤기 때문에 블러 애니메이션 효과 추가 작업은 의외로 간단하다.

    UIObject content ... //블러 필터 추가 filter = UIBlurFilter(): .filters += filter //1초 동안 애니메이션 가동 animation = UIAnimation(1.0)

    f = closure(UIAnimation animation, UIObject obj, Var progress):

    filter = UIBlurFilter():

    .power = 1 + 4 * progress //1초 동안 블러 단계는 1에서 5로 증가한다.

    obj.filters += filter


    animation.EventUpdated += {f, obj} animation.play()

    코드 20: 블러 애니메이션 추가

    블러 외에 다른 필터에도 같은 방식으로 애니메이션을 추가할 수 있다.


    4. 입력 이벤트

    시각 효과와 함께 입력 이벤트를 받고 처리하는 단계는 UI 시스템에서 고려해야 할 주부분에 해당한다.  여기서 입력 이벤트란 사용자 입력 내지 시스템 신호를 가리킨다. 사용자 입력은 마우스, 키보드, 터치, 음성 입력 등 존재한다. 시스템 신호는 어떤 특정 조건을 만족할 때 시스템에서 자체 발생한 신호를 가리킨다. 예로 시스템으로부터 전달된 메시지 신호가 있으며 이 입력을 받은 알림 앱은 사용자에게 관련 정보를 알려줄 수 있다.

    그림 14: 유저 인터렉션 수행 단계


    사용자 상호작용(User Interaction)은 입력, 조건 처리, 기능 수행, 상태 변경 네 단계를 포함한다. 본 절에서는 사용자 입력을 받는 입력 처리하는 단계에 초점을 둔다.



    4.1 입력 신호 전달

    여기서는 전통적인 입력 신호로서 키, 마우스 그리고 터치 신호에 초점을 둔다. 터치스크린이 주 입력 장치인 기기에서는 터치 신호를 추가로 받지만, 이는 근본적으로 마우스 신호와 큰 차이가 없다.

    현대의 대중적인 시스템에서 사용자 입력 신호는 입력 장치부터 커널을 통해 윈도우 서버로 전달된다. 여기서 윈도우 서버 역을 담당하는 윈도우 관리자는 입력 신호를 선 처리하는 과정을 수행할 수 있다. 이 과정에서 신호를 받아야 할 클라이언트(UI 앱) 대상을 결정하고 약속한 프로토콜을 통해 클라이언트에게 신호를 전달한다. 일반적으로는 활동 중인 UI 앱이 대상이 되며 신호는 IPC(Inter-Process Communication)를 통해 패킷으로 전달할 수 있다. 멀티 윈도우 환경에서는 여러 UI 앱이 동시에 활동할 수 있음으로 윈도우 관리자는 정책을 기반으로 입력 신호를 UI 앱으로 전달해야 한다. 여기서 포커스(Focus) 메커니즘은 다수의 활동 중인 UI 앱 중 이벤트 대상을 가리킬 수 있는 보편적 방안으로 적용할 수 있다.



    그림 15: 윈도우 시스템 입력 이벤트 전달 과정

    입력 신호는 여러 정보를 기록한다. 가령 어떤 장치에서 발생한 신호인지 구별하기 위한 ID, 신호가 발생한 시간 정보(Timestamp), 입력 신호의 화면 상 위치(Position), 키가 눌렸는지(Press/Release) 커서가 이동했는지(Move) 여부 등 정보가 이에 해당한다. 서버와 클라이언트 간 이벤트 전송은 비동기적이므로 클라이언트는 전달받은 신호를 입력 신호 큐(event queue)에 축적한다. 만약 UI 엔진이 입력 신호 송신을 전담하기 위한 스레드를 별도로 운용한다면 UI 앱의 메인루프는 입력 신호로 동작이 지연되는 것을 피할 수 있다. 이 경우 메인루프는 이벤트 대기 단계에서 입력 신호가 큐에 존재하는지 확인하고 있는 경우 이벤트 처리 과정으로 진입하면 된다. 그 후 입력 신호는 캔버스 또는 위젯 엔진, 필요에 따라 제스처 관리자(Gesture Manager) 등을 거친 후 최종적으로 대상 UI 객체까지 전달된다.

    그림 16: UI 컨트롤로 입력 이벤트를 전달하는 과정


    캔버스 엔진은 내부에서 관리하는 UIObject 객체 리스트로부터 이벤트를 받을 대상 객체를 찾아야 한다. (코드 21) 이는 객체 리스트를 순회하며 객체 영역(Bounding Box)과 이벤트 발생 위치를 비교하여 판단할 수 있다. 일반적으로 이벤트 발생 위치에 여러 객체가 중첩된 경우 최상단 객체가 이벤트 우선순위를 갖는다. 따라서 최상단 객체를 우선으로 이벤트를 호출한다. 만약 엔진에서 이벤트 전파(Propagation) 기능을 제공한다면 이벤트를 하단의 객체로 전달할지 말지 결정할 수 있다. (그림 17)

    그림 17: 중첩 객체 이벤트 처리 여부

    /*

    * 캔버스 엔진 수준에서 이벤트 처리 작업 수행

    * info는 UIEngine으로부터 전달받은 입력 이벤트 정보가 기록되어 있다.

    */ UICanvas.processEvent(info, ...):

    /* 이벤트를 전파할 대상 객체 탐색

    체 목록은 레이어 순으로 정렬되어 있다고 가정 */

    foreach(.objs, obj)

    if(obj.processEvent(info, ...) == false) break

    코드 21: UICanvas 이벤트 수행 로직

    UIObject.processEvent(info, ...): //활성 객체인 경우에 이벤트 수행 (2.3절 참고) if(obj.activate == false) return true //이벤트 발생 위치가 오브젝트 영역 내에 위치하는가? if(intersects(obj.geometry, info.position) == false) return true /* 자식이 존재하는 경우 자식에게 먼저 이벤트 전달

    자식 목록은 레이어 순으로 정렬되어 있다고 가정 */ foreach (obj.children, child) if(child.processEvent(info, ...) == false) break

    //해당 이벤트 타입으로 등록된 콜백 함수 목록 List eventCbs = obj.eventTable[info.type] //콜백 함수 순차적 호출 foreach(eventCbs, func) func(...)


    //true인 경우 다음 오브젝트로 입력 이벤트 전파 return obj.propagation

    코드 22: UIObject 이벤트 수행 메인 로직

    //객체의 이벤트 전파 여부를 위해 다음 인터페이스 제공 obj.propagateEvent = true

    코드 23: 이벤트 전파 설정 예

    코드 23 19번째 줄을 확인하면 UIObject는 이벤트 타입과 해당 이벤트 타입에 등록된 콜백 함수 목록을 테이블 형태로 구축하고 있는 점을 확인할 수 있다. 일반적으로 이벤트 종류는 Press, Unpress, Cursor In, Cursor Out, Move 등 원초적 형태를 갖추지만, Tab, Double Tab, Long Press, Flick 등 가공된 제스처 형태도 가능하다. 이는 윈도우 서버가 전달하는 이벤트 정의에 의존한다. 만약 UICanvas로 전달한 이벤트가 원초적 형태의 이벤트에 해당한다면 추가 과정을 거쳐 제스처 인식을 수행할 수 있다. 이 경우 캔버스 엔진(또는 위젯 엔진)은 제스처 타입을 정의하고 입력 신호로부터 제스처 발생 여부를 판단하여 최종적으로 UI 컨트롤로 가공된 제스처 형태의 신호를 전달한다.

    이벤트를 받은 오브젝트는 콜백 함수를 호출함으로써 해당 이벤트를 등록한 소스(Source)로 이벤트 정보를 전달한다(22번째 줄). 기본적으로 이벤트 대상 객체는 앱 또는 UI 엔진 내에서 콜백 함수(1장 2.2절 참고)를 등록한 객체로 한정한다. processEvent()은 자식을 포함한 콜백 함수가 존재할 경우에만 의미 있음으로 콜백 함수가 등록되지 않는 경우에는 적절한 조건 판단을 통해 로직 수행을 회피한다.


    4.2 제스처

    사용자 상호작용의 어떤 효과를 구현하려면 객체에 이벤트 콜백 함수를 등록하여 특정 기능 로직을 수행해야 한다. 이벤트 콜백 메커니즘은 앞 절에서 살펴보았으므로 사용자가 등록할 수 있는 제스처에 대해 조금 더 살펴보자.

    제스처(Gesture)는 UI 기능을 발동(Trigger)하는 입력 장치로서 다양한 입력 메서드를 정의한다. 그중 가장 일반적인 클릭(Click) 또는 탭(Tab) 제스처는 누름(Press)과 해지(Unpress) 입력이 연달아 발생할 경우 발동한다. 이때 누름과 해지 사이의 좌표 변화는 없거나 일정 범위 내에 있어야 하고 시차 또한 일정 범위(예: 0.2초) 내에 있어야 한다. 만약 두 입력 사이의 좌표 위치에 변화가 존재한다면 클릭이 아닌 플릭(Flick) 제스처로 해석할 수 있다. 또한 시차가 클릭 정의에서 벗어난다면 롱프레스(Long press) 제스처로 해석할 수 있다. 이처럼 제스처는 타입별 조건을 비교하여 발동 여부를 결정한다.

    제스처는 UI 시스템에 따라 종류 및 동작 정의가 다를 수 있으나 UX에서 통용되는 사용자 입력 형태를 정의하는 것이 바람직하다. 또한 앱은 입력 제스처를 직접 정의하지 않고 UI 시스템에서 제공하는 기능을 이용하는 편이 유리하다. 특히 이는 앱 간 동작 일관성 측면에서 중요하다. 따라서 UI 시스템은 견고한 제스처 인터페이스를 제공하고 앱 개발자가 해당 기능을 바로 이용할 수 있는 편의를 제공해야 한다.

    그림 19: 제스처 종류 예시 (EFL)

    기본적으로 제스처 이벤트는 윈도우 서버로부터 전달받은 원본 입력 이벤트를 가공하고 인터페이스를 통해 이를 앱에게 제공하는 것을 목표로 한다. 이를 위해 UI 프레임워크는 이를 전담하는 제스처 모듈(UIGesture)을 구현하고 이를 UI 엔진에 결합할 수 있다. 이로써 UI 앱은 UI 컨트롤을 이용하여 제스처 이벤트를 등록하고 앱 시나리오에 맞는 기능 동작을 구현한다.

    그림 20클라이언트 제스처 모듈 수행 단계


    특수 경우로 제스처 입력을 수행해야 한다면 윈도우 서버에서 제스처 이벤트를 정의하고 처리하는 것도 가능하다. 예를 들면 모바일의 퀵패널(QuickPanel)을 활성화하는 기능이 이에 해당한다. 사용자는 화면을 수직으로 드래그(Drag)함으로써 퀵패널을 활성화한다. 퀵패널 역시 윈도우 클라이언트에 해당할 수 있으나 윈도우 서버와 긴밀하게 이벤트를 주고받기 위해서 윈도우 관리자 모듈(그림 22)로 설계할 수 있다.


    그림 21: 모바일 퀵패널 (삼성 갤럭시)


    그림 22: 윈도우 서버 제스처 모듈 설계


    제스처 수행 조건을 변경할 수 있으면 구현이 더욱 유연하다. 만약 UI 시스템마다 제스처 수행 정의가 다르다면 여러 운영체제를 지원하는 앱에서는 그 정의를 변경해서 일원화해야 한다. 따라서 UI 시스템은 제스처 이벤트를 추가/변경할 수 있는 인터페이스를 고려하면 좋다. 새로운 제스처 타입은 제스처를 구현하는 클래스를 확장함으로써 가능하지만, 제스처 변경은 제스처 동작을 결정하는 요솟값(factor)을 변경함으로써 가능하다. 따라서 이러한 요소는 파라미터를 통해 외부로 노출한다. 한편, 제스처 변경으로 인해 앱마다 제스처 수행 방식이 다르다면 최종 사용자는 사용 방법에 혼란을 겪을 수도 있다. 따라서 제스처 커스터마이징 정책은 신중히 결정해야 한다.

    다음 목록은 제스처 커스터마이징 요소의 예시를 보여준다. 코드 24는 요소별 인터페이스를 제공하고 커스터마이징을 수행하는 과정을 보여준다.

  • tabTimeout: 탭: 누름, 해지 허용 시차
  • doubleTabTimeout: 이중 탭: 두 탭 간 허용 시차
  • tabFingerSize: 이중 탭: 두 탭 간 허용 거리
  • zoomDistanceTolerance: 줌: 두 좌표 간 최소 거리
  • zoomFactor: 줌: 두 좌표 사이 거리 변화 대비 줌 변화 비율
  • lineLength: 드래그: 직선 여부를 판단하기 위한 두 좌표 간 최소 거리
  • lineAngularTolerance: 드래그: 직선에서 벗어나는 각도 변화 허용 값
  • rotateAngularTolerance: 회전: 회전으로 인식하는 최소 각도
  • flickTimeLimit: 플릭: 누름, 해지 사이 허용 시차

  • //다음 UIGesture 설정은 앱에서 사용하는 제스처 기능에 전역적으로 영향을 미친다. //tabTimeOut을 0.5초로 지정 UIGesture.tabTimeout = 0.5

    //zoom 인식을 위해 두 탭 간 최소 거리를 100픽셀로 지정 UIGesture.zoomDistanceTolerance = 100 //줌 제스처 발생 시 해당 줌 팩터만큼 뷰를 확대 또는 축소 f = closure(UIView obj, UIGestureZoomInfo info):

    obj.scale = info.zoom


    view = UIView(): .EventZoom += f ...

    코드 24: 제스처 이용한 줌 인터렉션 구현


    코드 24의 10번째 줄을 확인하면 줌 팩터 값을 사용자에게 제공하기 위해 UIGestureZoomInfo 파라미터를 콜백 함수에서 정의하는 것을 확인할 수 있다. 여기서 UIGestureZoomInfo는 UIGestureInfo의 확장 타입으로서 줌 정보를 추가로 가진 구조체 정보로 볼 수 있다. 실제 인터페이스 설계 방안은 각 디자인 방침에 따르면 된다.


    제스처 커스터마이징 요소를 UI 시스템 환경 설정 데이터로 기술할 수 있다면 해당 UI 시스템을 이용하는 제품마다 그 특성을 쉽게 커스터마이징 할 수 있다. 이는 더욱 유연한 UI 프레임워크 시스템을 구축하는 데 도움을 준다. 



    5. 스레딩

    2.2절에서 언급했듯이 60fps 앱 성능을 보장하기 위해서는 UI 엔진의 메인루프가 한 사이클마다 처리해야 하는 작업은 0.0167초 안에 끝나야 한다. 이 시간 동안 앱은 비즈니스 로직부터 렌더링 엔진의 드로잉 작업을 완수해야 한다. 예로 그림 6.2의 사용자 애니메이션 수행 함수가 어떤 작업 부하로 시간을 지연시킨다면 메인루프는 0.0167초 내로 장면을 완성할 수 없을 것이다. 같은 맥락에서, 이벤트 처리 과정 중 많은 양의 데이터를 주고받거나 연산 처리를 수행한다면 동일 문제는 발생한다. 또한 앱에서 구현한 어떤 함수가 많은 시간을 소모한다면 UI 엔진의 메인루프는 그만큼 동작이 지연되며 화면 갱신 또한 느려질 것이다. 이러한 이유로 메인 루프의 동작 수행이 지체되어 장면을 제때 만들지 못한다면 프레임 손실이 발생한다. 프레임 손실은 특히 애니메이션 시 두드러지는데 매끄럽지 않은 애니메이션 효과로 인해 사용자 경험 만족도는 하락한다. 따라서 애니메이션 중 프레임 저하가 발생하는 시나리오는 최적화의 우선순위 대상이다.


    이와 같은 이유에서 UI 앱에서 많은 연산 작업이 필요한 경우 작업을 병렬로 처리하는 방법을 고려할 수 있다. 병렬 처리를 적용함으로써 가용한 다수의 컴퓨팅 유닛(CPU)을 적절히 활용하여 성능을 향상한다. 그뿐만 아니라 부하를 유발하는 작업을 별도의 스레드로 분리함으로써 메인루프의 지연을 방지한다. 핵심은 UI 엔진의 메인루프가 원활히 구동될 수 있어야 한다는 점에 있다.


    우리는 병렬 처리의 기본이 되는 메커니즘으로서 스레드(Thread)를 언급하지 않을 수 없다. 현대의 프로그래밍에서 스레드는 원시적인 방법에 해당하지만, 운영체제에서는 병렬 처리의 근간이 되는 개념에 해당한다. 스레드는 유닉스(Unix)를 기반으로 하는 임베디드 시스템과 여러 운영체제에서 지원하기 때문에 호환성이 뛰어나 소프트웨어 개발에서 응용할 수 있는 방법의 하나로 채택한다. 다만 예측 불가한 동작 순서는 비결정적(Non-deterministic) 동작 결과를 초래하고 이는 디버깅을 어렵게 하지만 섬세한 설계를 바탕으로 스레드 동기화(Synchronization)와 같은 도구를 잘 활용한다면 안정적이고 효율적인 결과를 만들 수 있다. 특히 UI 프레임워크는 사용자가 쉽고 안정적으로 병렬화를 적용할 수 있도록 UI 엔진과 상호작용하는 스레드 동작과 편의 인터페이스를 제공해야 한다.



    5.1 멀티 스레딩 전략


    시스템과 통합된 UI 엔진을 설계한다면 멀티 스레딩 환경에서 주 스레드는 메인루프에 우선 배정한다. 메인루프를 주 스레드에 배정하면 메인루프를 프로세스의 라이프사이클과 동일시 할 수 있으며 UI 앱의 라이프사이클을 관장하기 유리해진다. 이 경우 사용자는 작업 스레드(Worker Thread)를 추가하여 메인 루프 외 어떤 작업을 병렬화 할  수 있다. 여기서 만약 작업 스레드의 수행 결과물이 UI에 반영되어야 한다면 작업 스레드와 주 스레드 간 공유 자원을 안전하게 접근할 수 있는 동기화(Synchronization)를 수행해야 한다. 동기화 방안으로는 임계 영역(Critical Section)이 있으며 이 구간만큼은 메인루프도 지정된 사이클을 중단하고 작업 스레드와 데이터 공유 작업을 수행한다. 여기서 임계 영역이란 여러 스레드가 공유 자원에 동시에 접근하면 안 되는 코드 일부를 의미한다. 한편 작업 스레드의 결과물이 UI에 의존성을 갖지 않는다면 사용자는 작업 스레드를 독립 프로시저(Procedure)로써 비교적 쉽게 운용할 수 있다.


    그림 23: 메인루프와 작업 스레드간 동기화


    그림 23에서 임계 영역을 지정하기 위해 UITask(작업 스레드)에서 begin_critical_section()과 end_critical_section()을 호출한 것에 주목한다. 이때 UIEngine은 두 호출 구간에서 메인루프를 대기 상태로 만들고 작업 스레드가 UI 공유 자원에 안전하게 접근하는 것을 보장한다.

    5.2 태스크

    앞선 설명을 바탕으로 UIEngine은 고수준 병렬 작업을 제공하기 위해 UITask(태스크) 기능을 제공할 수 있다. UITask는 사용자가 스레드를 직접 사용하지 않고도 병렬 작업을 쉽게 구축할 수 있는 추상화한 개념을 제공한다. UITask 핵심 메서드를 살펴보면 다음과 같다.


    /* UITask는 UIEngine과 동기화를 수행하는 병렬(또는 직렬) 작업을 구축한다. 사용자는 작업

    병렬화를 위해 UITask로부터 사용자 클래스를 확장한다. UITask는 run()을 통해 실행되고 작업 스레드에서 task()를 수행한다. task()가 종료되면 동기화 작업을 위해 메인루프로부터 end() 메서드를 호출한다. 따라서 end()는 작업 스레드가 아닌 주 스레드에 해당하므로 동기화 작업을 따로 수행할 필요가 없다. */ UITask: /* 실제 수행할 동기/비동기 작업을 구현한다. 비동기(async) 경우 task()는 작업 스레드에서 호출되고 동기(sync)의 경우 메인루프를 구동하는 주 스레드에서 호출된다. */ task(): /* task()가 끝나면 호출된다. end()에서는 작업 결과물을 공유 자원(UI 객체)에

    반영한다. 또는 작업 후 리소스를 정리하는 작업을 수행할 수도 있다. */ end(): /* 스레드를 배정받고 task()르 실행한다. 필요하다면 메서드 인자나 컴파일러 내장 옵션을 통해 동기/비동기 수행을 결정할 수 있을 것이다. */ run():

    코드 25: UITask 핵심 메서드

    UITask는 작업 스케줄링에 따라 작업 스레드 또는 주 스레드에서 수행될 수 있다. 이러한 결과는 런타임 시 스케줄링 정책에 따라 다르므로 사용자에게 불투명한 동작을 제공하지만, UI 앱의 확장성에서는 유리하다. 스레드 가용 여부에 따라 사용을 자동으로 결정할 수 있기 때문이다. 그렇지 않다면, run() 메서드의 인자로 동기/비동기 옵션을 제공하고 사용자가 직접 결정할 수 있는 선택 사항을 제공하거나 UISyncTask/UIAsyncTask와 같은 동기/비동기 작업 클래스를 분리하여 제공할 수도 있다. 이러한 부분은 설계 지침에 맡긴다.

    이제 사용자는 UITask를 확장하여 실제 수행할 작업을 구현한다.

    /* 사용자는 비동기 작업을 수행하기 위해 UITask로부터 UserTask를 구현한다. */ UserTask extends UITask: /* UIImage는 캔버스 엔진에 종속된 공유 자원에 해당 동기화를 무시한다면 작업 스레드에서 해당 객체에 접근 시 캔버스 엔진을 수행하는 주 스레드와 불안정한 경합이 발생할 수 있다. */ UIImage img constructor(): //img 초기 설정 수행 ... /* 여기서 어떤 Heavy한 작업을 한다고 가정하자. */ override task(): /* 원격으로 어떤 대용량 이미지를 받아온다. 내려받기(Download)를 완수하면 task()를 종료한다. */ ... /* task() 완료 후 수행할 종료 작업 */ override end(): //내려받은 이미지를 이미지 객체로 출력 .img.path = “/tmp/downloaded.png”

    코드 26: UITask를 이용한 비동기 작업 구현

    마지막으로 사용자는 준비한 작업을 주 스레드에서 호출한다.

    /* func()은 주 스레드에서 동작하는 어떤 함수다. */ func(): task = UITask(): .run()

    코드 27: UITask를 이용한 비동기 작업 수행

    코드 27에서 task 객체를 생성한 시점은 주 스레드에서 수행 중인 어떤 func()에 해당한다. 따라서 이 시점에 호출된 UserTask 생성자는 아무 문제 없이 이미지 객체를 생성하고 초기 설정을 수행한다. 이후 run()을 호출하면 UITask는 내부적으로 UI 엔진에 task()를 수행할 작업 스레드를 요청하고 메인루프와 병렬로 작업을 수행한다. task()를 수행한 작업 스레드가 종료하면 UI 엔진은 작업 스레드로부터 종료 신호를 전달받는다. 이는 비동기 신호로서 스레드 간 통신을 수행할 수 있는 IPC 메커니즘을 이용할 수 있는데 대표적으로 유닉스 시스템에서는 메시지, 파이프(Pipe), 파일 디스크립터(File Descriptor) 등을 활용할 수 있다. 결과적으로 UITask는 task()를 종료하는 시점에 UIEngine(주 스레드)으로 작업 스레드 종료 이벤트를 전달한다. 이후 UI 엔진은 메인루프의 이벤트 처리 단계에서 해당 신호를 확인하고 UITask의 end()를 호출하여 사용자가 준비한 이미지를 화면에 출력할 수 있게 한다.

    여기까지 우리가 기대하는 동작 시퀀스는 다음 그림과 같다.

    그림 24: 작업 스레드 기반 UITask 수행 절차


    그림 24에서 사용자 로직과 UI 엔진 즉, 메인루프는 주 스레드에서 절차적으로 수행되는 한편 태스크는 작업 스레드에서 수행된다. 그리고 UI 엔진은 두 스레드 간 작업 조율을 담당한다. 이를 위해  UI 엔진은 메인루프의 이벤트 처리 단계에 Task 요청 메시지를 해석하고(dispatch) 스레드 작업을 개시한다. (그림 25) UI 엔진은 사용자로부터 요청받은 작업을 수행할 스레드를 준비하고 작업을 실행하며 작업 스레드는 작업을 완수하면 UI 엔진으로 비동기 메시지를 보내어 작업 완료 사실을 알린다. 이후 메인루프 사이클을 수행하고 있던 UI 엔진은 다시 이벤트 처리 단계에서 해당 메시지를 확인하고 task.end()를 호출하여 사용자가 안전하게 작업을 완료할 수 있도록 한다.


    그림 25멀티 스레드 기반 태스크 상호 운용


    그림 25에서 태스크 메시지(Task Msg)는 일반 이벤트로서 취급한다. 이는 그림 16의 사용자 입력 신호를 전달하는 방법과 동일하다. 하지만 실제 UI 엔진 운용 중 필요한 이벤트와 메시지는 복잡하고 다양하다. 따라서 이벤트를 더욱 정교하게 처리하기 위해서는 이벤트 형식마다 최적화된 이벤트 수행 루틴을 구축하는 것을 고려할 수 있다.


    5.3 크리티컬 세션

    앞서 살펴본 바에 의하면, UITask를 통해 어떤 작업이 완성하고 그 작업 결과물은 end() 메서드를 활용하면 단발성을 띈 작업을 병렬로 수행하는 것은 문제되지 않을 듯 하다. 하지만 비동기 작업을 진행하면서 작업 결과물을 주기적으로 반영해야 한다면 동기화 역시 주기적으로 요청해야 한다. 이를 위해 UIEngine에서 임계 영역을 지정할 수 있는 방법을 추가로 고려해 볼 수 있다.

    override UserTask.task(): /* 여기서 어떤 Heavy한 작업을 거쳐 임의 수의 이미지를 실시간으로 생성한다고 가정하자. 생성된 이미지는 즉시 이미지 객체를 통해 화면에 출력해야 한다. */ repeat (infinite) Pixel bitmap[] = generateImage(...) /* beginCriticalSection()을 호출하면 메인루프가 동작을 중단하여 작업 스레드에서 공유 자원에 안전하게 접근할 수 있도록 도와준다. 동기화 작업이 끝나면 endCriticalSection()를 호출하여 메인루프가 재개하도록 한다. */ UIEngine.beginCriticalSection() .img.load(bitmap) UIEngine.endCriticalSection()

    코드 26: 크리티컬 세션을 통한 동기화 작업 수행

    그림 23과 같이 beginCriticalSection()과 endCriticalSection() 사이는 메인루프가 동작을 멈추는 구간으로서 주 스레드가 공유 자원에 접근하지 못하도록 방지하는 역할을 수행한다. 작업 스레드에서 UIEngine.beginCriticalSection()을 호출하면 주 스레드로 스레드 동기화 요청 메시지를 보낸 후 작업 스레드는 그 결과를 받기 위한 모니터링 상태(또는 대기 상태)로 전환한다. 이 후 지정된 메인루프 사이클을 돌던 주 스레드는 이벤트 처리 단계에 도달 시 요청한 메시지를 해석한다. 이 후 주 스레드는 요청한 대로 메인루프의 동작을 일시 중지하고 작업 스레드가 임계 영역 구간을 수행할 수 있도록 동작 재개 요청 메시지를 보낸다. 이 후 주 스레드는 endCriticalSection()이 불릴 때까지 모니터링(대기) 상태에 빠지며 주 스레드로부터 UI 객체에 접근하는 일이 없도록 방지한다. 한편, 모니터링 상태에 있던 작업 스레드는 주 스레드로부터 동작 재개 메시지를 전달 받고 그 즉시 대기 상태에서 깨어나 이 후 작업을 진행한다. 이 후 작업 스레드는 endCriticalSection()을 호출하면서 주 스레드와 동기화 작업을 동일한 방식으로 수행한다.

    그림 26: 임계 영역 수행 시퀀스

    beginCriticalSection()과 endCriticalSection() 구간은 임계 영역으로서 주 스레드는 대기 상태로 존재한다. 그로 인해 메인루프가 동작을 멈추며 화면 갱신은 지연된다. 따라서 매끄럽게 동작하는 UI 앱을 구현하기 위해서는 임계 영역을 최소로 지정해야 한다.


    5.4 태스크 스케줄링

    이론상 하나의 프로세스는 가용한 물리적 스레드(Physical Thread) 수와 동일한 수의 논리적 스레드(Logical Thread)를 운영하는 것이 이상적이다. 물리적 스레드보다 많은 논리적 스레드를 생성한다면 스레드 전환 작업에 시간을 낭비하여 오히려 성능 하락 요소로 작용할 수 있다. 따라서 시스템은 최적의 스레드 운용을 위한 스레드 관리를 고려해야 한다. 만약 태스크마다 스레드를 생성한다면 스레드 과부하가 발생할 수 있다. UI 엔진은 작업 스레드 수에 제한을 두고 다수의 태스크를 효율적으로 처리할 수 있어야 한다.

    • 스레드 풀 (Thread Pool)
    멀티코어 시스템이 도래한 후 스레드 풀 기능은 보편적으로 사용된다. 일부 현대의 프로그래밍 언어에서는 스레드풀 기반 태스크 스케줄링을 자체적으로 지원하기도 하지만 비교적 원시적 환경이라면 태스크 스케줄링을 자체적으로 구현해야 한다. 그것과 별개로 특정 시스템 중심의 고성능 태스크 스케줄링을 구현하는 것도 타당하다.

    그림 27: 스레드풀 기반 태스크 스케줄링


    그림 27은 일반적인 스레드 풀 기반 태스크 스케줄링을 도식화한다. 태스크 스케줄링을 구현하는 태스크 스케줄러(Task Scheduler)는 적정 수의 스레드와 작업 큐(Task Queue)를 미리 생성한다. 요청받은 작업을 스레드에 바로 배정하지 않고 작업 큐에 추가한다. 운행 중인 스레드는 작업을 완수할 때마다 작업 큐에서 작업을 추가로 취득하고 작업을 계속 진행한다. 작업 큐에 담긴 작업은 선입선출(First-In First-Out) 방식으로 처리한다. 스레드가 처리할 작업이 작업 큐에 존재하지 않다면 스레드는 추가 작업 요청이 올 때까지 대기한다. 스레드가 완수한 작업은 완료 큐(Completed Queue)에 추가하고 추후 주 스레드에서 이를 확인한다. 또는 완료 큐를 거치지 않고 곧장 호출자(사용자)에게 이벤트를 전달할 수도 있다.

    앞서 살펴본 개념을 토대로 UIEngine이 사용자가 요청한 작업을 태스크 스케줄링을 기반으로 처리한다면 작업을 더욱 효율적으로 처리할 수 있다. 시스템의 CPU 코어 또는 물리적 스레드 수만큼 스레드를 생성하고 UI 앱 또는 UI 시스템에서 생성한 작업을 태스크 스케줄링으로 일괄 처리한다면 더욱 효율적인 작업 처리를 할 수 있다. 그리고 이는 곧 성능 최적화로 이어진다.

    다음 그림은 이러한 태스크 스케줄링 기반 작업 수행 과정의 단적인 예를 보여준다.

    그림 28: 태스크 스케줄링 기반 태스크 수행 시퀀스



    6. 정리하기

    애니메이션은 UI 앱을 보다 풍성하게 해주는 핵심 기능으로서 UI 엔진에서 반드시 다뤄야 하는 요소 중 하나이다. 이번 장에서 우리는 애니메이션 이해와 사용자가 애니메이션을 구현하는 데 있어서 필수 요건에 대해 살펴보았다.


    기본적으로 애니메이션은 연속된 장면을 한 장씩 출력함으로써 표현한다. 이를 위한 메커니즘을 프레임 애니메이션을 통해 살펴보았다. 메인루프를 기반으로 애니메이션 프레임 번호를 결정하고 사용자가 애니메이션 장면을 결정하기 위한 사용자 인터페이스도 살펴보았다. 그뿐만 아니라 부드러운 애니메이션 표현하기 위한 프레임 시간 계산 및 제어 방법을 살펴보았고 애니메이션 가속 방식과 이를 커스텀 할 수 있는 메커니즘도 함께 살펴보았다. 


    여러 애니메이션 기법 중 UI 객체의 속성에 변화를 둔 프로퍼티 애니메이션에 대해서 살펴보았다. 이러한 애니메이션 동작을 기반으로 필터 애니메이션을 적용해 봄으로써 애니메이션 적용 사례에 대한 이해도를 높였다. 추가로 사용자 상호 작용을 위해 사용자 입력 이벤트 처리 과정을 살펴보았고 나아가 제스처 인식의 고수준 이벤트 제공 방안도 살펴보았다.


    마지막으로 스레드 풀을 적용한 고수준 태스크 스케줄링과 비동기 작업 수행 메커니즘을 살펴보았다. 이 과정에서 UI 엔진의 메인루프와 함께 여러 작업을 동시에 수행할 수 있는 고성능 UI 엔진을 구축 방안도 함께 이해할 수 있었으며 이러한 병렬 처리 기반으로 원활한 애니메이션 엔진을 구축할 수 있음을 배울 수 있었다.