brunch

You can make anything
by writing

C.S.Lewis

by 김민태 Dec 29. 2018

FramerX 스터디 연재#1.2

아주 심플한 인터렉션 만들어보기

1. FramerX 스터디 연재#1.1 - 코드 작성을 위해 알아야 할 기초 개념


연재#1.1에서 FramerX가 제공하는 샘플 코드를 읽을 수 있도록 기초적인 개념에 대해 설명했다. 이번엔 실제로 간단한 인터렉션을 만들어 이해의 폭을 한 걸음 더 넓혀보자. 만들어볼 간단한 샘플은 탭 할 때마다 작아졌다, 커졌다를 반복하는 사각형 도형이다.

탭하면 커지고 다시 탭하면 작아지는 버튼

먼저 새로운 프로젝트를 하나 만들고 마음에 드는 형태의 버튼을 만든다. 만든 버튼을 선택한 후 속성 창의 Code 탭에서 새로운 New File을 선택하여 코드 에디터가 실행되도록 하자.


App.tsx


새로운 파일을 만들면 App이라는 이름으로 코드 파일이 생성된다. 새로운 파일을 만든 특별한 이유는 없다. 원한다면 기본 제공되는 Examples.tsx 파일에서 필요한 코드를 작성해도 된다. 그냥 만들어서 하고 싶었다. Examples.tsx 만큼 많은 코드가 제공되지는 않지만 간단한 Scale 함수 코드가 이미 작성되어 있는 걸 알 수 있다. 기존에 있던 코드는 그대로 두고 그 아래에 다음의 코드를 추가해 보자. (기존 코드를 복사해 붙여 넣기 한 후 이름만 바꾼 것이다)

이렇게 ToggleButton 코드를 작성한 후 저장하면 FramerX 편집기의 Override에 ToggleButton 이 나타나게 된다. 자, ToggleButton 함수 안에 있는 return { ... } 코드의 의미에 대해 탐구해 보자.


객체를 반환하면 누가 받지?


함수는 값을 돌려준다고 했다. 누구에게? 호출한 코드에게 돌려준다고 했다. 그런데 App.tsx 어디를 봐도 ToggleButton 함수를 호출하는 코드를 발견할 수 없다. ToggleButton() 이렇게 호출하는 코드를 작성해야 할까? 아니다. 사실 ToggleButton 함수는 FramerX 가 호출하도록 되어있다. FramerX 편집기의 Override 목록에서 ToggleButton을 선택했다는 것은 코드 관점에서 해석하면 프로토타입이 실행될 때 ToggleButton 함수를 호출되도록 설정하는 행위인 것이다.


Framer의 실행(Preview) 버튼을 클릭해서 실행될 때 자동으로 ToggleButton 함수가 호출되며 ToggleButton 함수가 반환하는 값(객체)을 Framer가 해석하여 어떤 동작을 수행하도록 약속된 것이다. 우리가 반환하는 객체에 어떤 값을 담아 넘겨주면 어떤 일이 일어나는지 알면 그것들을 이용하여 우리가 원하는 목적을 달성할 수 있다.


반환하는 객체는 크게 두 가지 타입의 값을 담을 수 있다. 속성 값과 함수 값이 그것이다. 위 코드를 보면 scale 가 속성 값이고 onTab 이 함수다. 음? 지금까지 배웠던 2가지 함수 모양과는 또 다르다!!! 우리가 배운 함수의 모양은 다음과 같았다.

const ToggleButton1 = function() {
    ...
}

const ToggleButton2 = () => {
   ...
}

여기에 새롭게 등장한 onTab() { ... } 이건 뭘까? 이것 또한 함수 표현을 축약한 문법이다. 아래 세 가지 함수 표기 방법 표현은 완전히 동일하다.

return {
    onTap1: function()  {

    },

    onTap2: () => {

    },

    onTap3() {

    }
}

onTap1, onTap2, onTap3 모두 객체 내에 포함된 함수다. 객체에 값을 담을 땐 { a = 10 } 이렇게 하지 않고 { a: 10 } 이렇게 이름과(변수) 값을 구분한다고 했다. 여기에 값 자리에 함수가 들어가게 되는 것이다. 초기 분법으론 onTap1 형태를 사용했다. 이후 화살표 함수가 등장하여 onTap2처럼 사용할 수 있게 되었고 더욱더 나아가 객체 내에선 함수를 더 간단히 표현할 수 있는 표현인 onTap3 형태가 추가된 것이다. 세 가지 중 어떤 방식을 사용해도 된다. 여러분이라면 어떤 방식을 선택하겠는가? ^^


함수 형태를 익히느라 옆길로 조금 샜다. 다시 return 하는 객체의 내용에 집중해 보자.


반환하는 객체의 값이 함수가 아닌 경우 그 이름이 FramerX 컴포넌트(도형이라고 생각하자)의 속성과 일치하면 그 값이 컴포넌트에 반영된다. scale 값이 있다면 그 값대로 컴포넌트의 scale이 변경된다. opacity라는 값이 있다면 컴포넌트에 opacity 가 반영되는 식이다.

반환하는 값에 따라 반응하는 연결된 컴포넌트

이제 컴포넌트에 변화를 주길 원할 때 우린 그 속성 이름이 무엇이고 어떤 형태의 값을 넣어야 하는 것인지만 알면 간단히 코드로 컴포넌트를 조작할 수 있게 되었다. 물론 아직 문서가 제대로 제공되지 않아 하나하나 찾아봐야 하는 불편함이 존재하긴 한다.


그럼 이제 값 타입 중 함수에 대해 알아보자


이벤트 함수


반환 객체의 함수는 이벤트 핸들러 함수라 한다. 속성 값이 연결된 컴포넌트의 특성에 관여한다면 함수는 연결된 컴포넌트의 어떤 사건(이벤트라 한다)이 발생했을 때 실행되어야 하는 코드를 제공하는 방식이다. 데이터가  상태를 의미한다면 코드는 행위를 묘사한다. 예를 들어 연결된 컴포넌트를 사용자가 터치하면 tap이라는 이벤트가 발생한다. 이때 우리가 컴포넌트 크기를 변경하고 싶다면 onTap이라는 약속된 이름의 함수로 컴포넌트의 크기를 조정하면 되는 것이다. onTap 함수는 tap 이벤트가 발생하면 Framer에 의해 호출된다.


이제 onTap 함수를 작성해보자. 우리의 목표였던 연결된 컴포넌트의 크기를 작게 만들어 볼 것이다.

onTap() {
   data.scale = 0.5
}

scale 값은 1 이 100%를 의미한다. 0.5라면 50%. 2라면 200%.  저장하고 실행하면 박스를 탭 하면 절반 크기로 줄어드는 것을 볼 수 있다.

아마도 data.scale = 0.5 이렇게 코드를 쓰면 에디터에서 오류가 표시될 수도 있을 것이다. 실제로 에디터에 표시된 오류를 없애기 위해선 data.scale.set(0.5) 이렇게 작성하거나, data.scale = Animatable(0.5) 이렇게 작성하면 된다. 실행 결과는 일단 모두 같은데 이 미묘한 차이는 좀 더 스터디를 진행한 후 밝혀 보도록 하자. 지금은 설명한다 해도 이해하기 힘들고 고통만 가중될 뿐이니 말이다. ^^

첫 번째 관문은 넘었다. 탭을 했을 때 반응하는 코드를 작성한 것이다. 이제 다음 목표로 한 발 더 나아가 보자.


상황에 따라 A 동작 또는 B 동작하기


우리의 최종 목표는 탭을 할 때마다 작아지고, 커지는 - 정확히는 원래 크기로 돌아오는 - 것이다. 지금까지 작성한 코드는 첫 번째 탭이 일어날 때 작아지고 그 이후에는 아무 변화가 없다. 실제로는 탭 할 때마다 0.5로 scale 이 변경되지만 현재 크기가 0.5이니 변화가 보이지 않는 것이다.


이제 해야 할 일은 탭이 발생했을 때 현재 크기가 0.5 인지 1 인지 알아내서 0.5이면 1로 바꾸고 1이라면 0.5로 scale을 변경하는 것이다. 이런 비교를 위해 if 문이라는 문법이 제공된다.


if 문

만약에 (A와 B를 비교했더니) 비교의 결과가 맞다면 { A-Code 코드를 } 틀렸다면 { B-Code 코드를 실행 }

if (A === B) {
    A-Code
} else {
    B-Code
}

이렇게 생겼다. if () { } else { } 첫 번째 괄호에 비교를 하며 비교의 결과가 참이면 괄호 다음의 { } 코드들이 실행되고 비교의 결과가 틀렸다면 else 다음의 { } 코드들이 실행된다. 비교는 다음과 같이 할 수 있으며 비교의 대상은 하나 또는 둘이 될 수 있다. 셋을 비교하는 경우는 없다.


(A === B)    A와 B가 같은가? 같으면 참(true) 다르면 거짓(false)

(A !== B)    A와 B가 다른가? 다르면 참(true) 같으면 거짓(false)

(A < B)    A가 B보다 작은가? 작으면 true 크거나 같으면 false

(A > B)    A가 B보다 큰가? 크면 true 작거나 같으면 false

(A <= B)    A가 B보다 작거나 같은가? 작거나 같으면 true 크면 false

(A >= B)    A가 B보다 크거나 같은가? 크거나 같으면 true 작으면 false


비교를 할 때 주의할 점은 비교하는 두 개의 값은 언제나 타입이 일치해야 한다는 것이다. 숫자와 숫자를 비교하거나 문자와 문자를 비교해야 원하는 결과를 얻을 수 있다. 값의 타입이 다른 두 개의 값을 비교한다면 결과가 true가 될지 false가 될지 예측하기 어렵다. (사실 거의 대부분 false이 된다)


이제 우리의 코드로 돌아와 if 문을 적용해 보자

onTap() {
    if (data.scale === 0.5) {
        data.scale = 1
    } else {
        data.scale = 0.5
    }
}

우리의 의도는 이렇다. 탭이 일어났을 때 만약 scale 값이 0.5 라면 작은 크기 상태일 것이니 원래 크기인 1로 변경한다. 그렇지 않다면 원래 크기인 1일 것이니 절반 크기인 0.5로 변경한다.


의도한 대로 잘 될 것 같지만 실행해 보면 결과는 첫 번째 탭이 발생했을 때 작아지고 그 이후 변화가 없다. 이유는 data.scale === 0.5 비교 결과가 언제나 false 이기 때문이다. 아니 왜????


사실 data.scale 값은 숫자 타입이 아니라 객체다. 우리가 비록 data.scale = 1 이렇게 숫자 타입 값을 넣었지만 그럼에도 불구하고 숫자 타입이 아니라 객체다. 이유는 data.scale 이 처음 만들어질 때 넣었던 값이 숫자가 아닌 Animatable(1) 함수의 반환 값이었기 때문이다. 뭐가 어떻게 돌아가는 건지 모르겠지만 Animatable함수가 반환한 값은 숫자가 아닌 객체이며 이 객체는 이렇게 생겼다.

{
    value: 1
}

음... 그렇다 해도 data.scale = 0.5 하면 0.5가 들어가는데? 맞다 잘 들어간다. 지금은 이해하기 힘든 좀 더 마법 같은 메커니즘이 내부에서 작동하고 있기 때문인데 현재 단계에서 학습하기엔 무리이니 조금 안타깝더라도 그냥 그렇군 하고 넘어가는 대범함을 연출해 보자.


난 정말 이 비밀을 죽어도 알고 싶고 공부해 보고 싶어 잠도 안 온다고!! 하시는 분을 위한  링크
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Proxy


그래서 대범한 우리는 코드를 다음과 같이 바꾼다.

onTap() {
    if (data.scale.value === 0.5) {
        data.scale = 1
    } else {
        data.scale = 0.5
    }
}

근사하게 동작하는 결과를 확인할 수 있을 것이다. 하지만 뭔가 아쉽다. 크기가 변하기만 할 뿐이기 때문이다. 이제 변할 때 애니메이션을 만들어 보자. 애니메이션은 놀랍도록 간단하다. 복잡한 동작을 FramerX가 모두 처리해 주고 우리는 간단히 몇 개의 옵션 값만 지정해 주면 그만이기 때문이다.

onTap() {
    if (data.scale.value === 0.5) {
        animate.spring(data.scale, 1)
    } else {
        animate.spring(data.scale, 0.5)
    }
}

애니메이션을 위해 FramerX는 animate 객체를 제공한다. 이 객체는 애니메이션 스타일에 ease, easeIn, easeInOut, easeOut 그리고 우리 모두가 좋아하는 spring 함수를 제공한다. 다섯 개의 애니메이션 함수는 첫 번째 값으로 애니메이션의 시작 값, 두 번째 값으로 애니메이션의 종료 값을 입력받도록 되어있다. 그리고 세 번째 값은 객체인데 애니메이션과 관련된 여러 가지 옵션을 줄 수 있다. 세 번째 인자를 생략하면 모두 기본값으로 처리된다.


이제 거의 다 왔다. 조금만 힘 내보자.


애니메이션까지 적용한 예제를 실행해보면 잘 되는 것처럼? 보인다. 어떤 문제가 있는가 하면 빠르게 탭을 해보면 뭔가 잘 안된다. 버튼이 반응하지 않는 구간이 있는 것이다. 이유는 뭘까?


상태를 직접 비교하지 않기


문제는 비교의 대상에 있었다. if (data.scale.value === 0.5) 이 코드 말이다. 무슨 문제일까? 애니메이션의 원리를 생각해보면 원인을 찾을 수 있다. 우리가 다루는 애니메이션은 값이 특정 시간의 흐름에 따라 변화하는 것이다. 즉, scale 값이 0.5에서 1로 변화할 때 한 번에 변경되는 것이 아닌 일정 시간 동안 다양한 형태로 변경된다. 0.5, 0.5123, 0.6234 .... 1.32422, 1.21232, .... 1 이런 식으로 말이다. 문제가 발생한 원인은 애니메이션이 진행 중인 시간을 고려하지 않은 값의 비교 때문에 일어난 것이다. 애니메이션이 진행 중일 때 탭을 하면 scale.value 는 0.5가 아닌 값이기 때문에 언제가 비교의 결과가 false이었던 것이다. 이 문제를 어떻게 해결할 수 있을까? 해결책은 비교를 직접적으로 하지 않고 간접적으로 하는 것이다. 간접적으로 비교를 한다는 건 무슨 뜻일까?


사실 우리가 알고 싶은 상태는 data.scale 값이 아니다. 우리가 알고 싶은 상태는 박스가 원래 크기인 상태인가? 아니면 작은 크기 상태인가? 이다. 원래 크기가 얼마이고 작은 크기가 얼마이던지 간에 말이다. 그래서 그 상태를 가지고 있는 전용(?) 값을 하나 만들어 그 값을 비교하면 모든 문제가 해결된다.


모든 것이 해결된 최종 코드

isOriginalSize 변수를 하나 만들었다. 이 변수는 true, false 둘 중 하나의 값만을 가지고 있다. 각각이 의미하는 바는 박스의 크기가 원래 크기라면 true. 작은 크기라면 false이다. 이 값을 비교의 대상으로 하니 민감한 scale.value 값에 의존하지 않아 빠른 탭이 발생하더라도 문제없이 반응하는 인터렉션을 완성할 수 있었다.


마지막으로


아무리 자세히 설명한다 해도 여전히 미지의 영역 같아 보이는 부분이 많이 존재할 것이다. 그렇다 해도 너무 성급해하지 말자. 이 연재는 가능한 길고 길게~ 이어갈 예정이니 여유롭고 즐겁게 험난한 대륙을 함께 탐험해 보자.


참고


App.tsx - https://gist.github.com/ibare/7f92bd9729dd854b320dc849440d35a6


매거진의 이전글 FramerX 스터디 연재#1.1
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari