기본 제공되는 많은 도구를 활용하는 첫 번째 방법
1. FramerX 스터디 연재#1.1 - 코드 작성을 위해 알아야 할 기초 개념
2. FramerX 스터디 연재#1.2 - 간단한 인터렉션 예제와 함께 동작 원리 학습
FramerX는 클래식 버전에서 많은 것이 달라졌다. 대부분 새롭게 학습해야 할 대상이지만 알아두면 앞으로 아주 도움을 많이 받을 새로운 기능을 간단한 인터렉션을 만들어 보며 알아보자.
이번 연재의 프로토타입은 탭 하고 있으면 계속 커지는 버튼이다. 손을 떼면 원래 크기로 빠르게 돌아간다. 버튼이 일정 크기 이상 커지면 배경 색이 반응한다. 점 점 빨간색으로 변한다. 페이스북 메신저를 사용해 봤다면 좋아요 버튼을 눌렀을 때 나오는 인터렉션과 유사하여 금방 이해할 수 있을 것 같다.
새로운 프로젝트를 만든 후 적당한 크기의 스크린 프레임을 생성한다. 스크린과 동일한 크기를 가진 프레임을 하나 만들고 그 위에 버튼 역할을 담당할 프레임을 만든다. 아래 이미지를 참고해서 적당히 만들면 되겠다. 참고로 좋아요 버튼은 적당한 모양의 이미지를 붙여 넣기 해서 만들었다.
연재#1.2에서 했던 것처럼 Code 탭에서 New File을 선택하여 App.tsx 파일을 생성한다. 기본 제공되는 Scale 함수의 이름을 적당히 변경한다. 점 점 더 커지는 기능의 코드를 작성한다는 의미로 KeepGrowingButton라고 변경했다. 그럼 코드의 모양은 아래와 같을 것이다.
FramerX 편집기에서 like(좋아요 모양의 이미지) 컴포넌트를 선택한 후 Code 탭에서 KeepGrowingButton을 선택해 연결해 준다. Preview를 실행시켜 잘 연결되었는지 확인한다. 탭 하면 작아졌다 커지는 인터렉션을 볼 수 있을 것이다. 이제 기본적인 준비가 되었으니 본격적으로 원하는 인터렉션을 만들어 보자.
소프트웨어를 만들기 전에 결정해야 하는 두 가지가 있다. 첫 번째 어떤 동작을 실행할 것인가 결정한다. 동작이 결정되면 그 동작을 수행하기 위해 어떤 상태가 필요한지 상태를 디자인한다. 우선 우리가 만들어야 하는 동작에 대해 생각해 보자.
1. like 버튼을 탭 하면 like 버튼이 커진다.
2. 손을 떼지 않고 있으면 like 버튼은 점 점 더 커진다.
3. like 버튼이 일정 크기 이상 커지면 스크린의 배경색이 점점 더 붉은색으로 변한다.
4. 손을 떼면 like 버튼은 원래 크기로 돌아간다.
5. 버튼이 원래 크기로 돌아가기 시작하면 배경색도 원래 색인 흰색으로 돌아간다.
동작을 설계할 땐 최대한 작은 단위로 상세히 묘사한다. 그리고 실제 일어나는 순서대로 묘사하는 것이 중요하다. 코드는 순서에 맞게 차례대로 실행되기 때문이다. 동작 묘사가 끝나면 다음 순서는 동작 실행에 필요한 상태를 알아내는 것이다.
동작이 일어나기 위해 필요한 상태는 어떤 것이 있는가?
1. 버튼의 기본 크기 값 - scale
2. 버튼의 커져야 하는 크기 값 - scale
3. 배경의 기본 색상 값 - background color
4. 배경의 붉게 변하고 있는 색상 값 - background color
5. 버튼의 크기가 계속 커져야 하는지 판단할 수 있는 값 - Yes/No
상태는 결국 값을 뜻한다. 상태는 독립적인 값도 있고, 어떤 상태에 종속적인 값도 있다. 예를 들어 기본 색상 값, 커져야 하는지 그렇지 않아야 하는지와 같은 값들은 독립적인 상태라고 볼 수 있다. 종속적인 생태는 어떤 것일까? 버튼이 커져야 한다는 상태 값에 따라 버튼의 크기 값은 점 점 더 커지게 될 테니 종속적인 상태라고 할 수 있다.
동작과 상태 설계를 마쳤으니 이제 이것을 코드로 변환해 보자.
동작은 일반적으로 사용자의 행동에서 시작된다. 각각의 행동에 대응하도록 이벤트를 제공한다. 앞서서 만들어본 예제에선 onTap 이벤트 핸들러 함수가 터치 이벤트를 처리했다. 그러나 이 이벤트 핸들러로는 우리가 원하는 동작을 만들어 낼 수 없다. 버튼을 터치하는 행위를 우리는 누르고 있는 동안, 그리고 손을 떼었을 때 각각 다른 동작을 만들어 내야 하기 때문이다. onTap 이벤트는 누른 후 손이 떨어지는 행위 하나를 알려주기 때문에 세밀한 동작을 만들어 낼 수 없는 것이다.
터치라는 행위를 좀 더 세밀하게 쪼개 보면 터치하는 대상에 손이 닫고, 손이 떨어지기 전까지 유지되다가 손이 대상에서 떨어지는 것이다. FramerX는 각각의 상황에 대응하는 이벤트를 제공한다.
1. onTapStart: 대상에 탭이 시작될 때 발생
2. onTapEnd: 대상에 탭이 끝날 때 발생
3. onTap: 1, 2가 발생된 직후 발생
1과 2 사이 걸리는 시간은 사용자가 얼마나 오랫동안 손을 떼지 않는가에 따라 달라진다. 터치라는 행위에 있어 이벤트는 언제나 1, 2, 3번의 순서로 발생한다.
이제 이벤트를 알았으니 각각의 이벤트 핸들러 함수에서 해야 할 작업을 생각해 볼 수 있다.
1. onTapStart에선 버튼을 점 점 더 크게 만든다.
2. onTapEnd에선 버튼을 원래 크기로 변경한다.
좋아요 버튼의 크기 값은 likeScale로 만들었다. 코드로 작성해 보면 다음과 같다.
const data = Data({
likeScale: Animatable(1)
})
export const KeepGrowingButton: Override = () => {
return {
scale: data.likeScale,
onTapStart() {
animate.ease(data.likeScale, 2);
},
onTapEnd() {
animate.spring(data.likeScale, 1);
}
}
}
이 코드를 동작시켜보면 터치하자마자 버튼은 2배로 커지고 아직 손을 떼지 않았음에도 버튼이 커지진 않는다. 손을 떼면 버튼이 원래 크기로 돌아간다. 코드를 살펴보면 onTapStart에서 애니메이션 값이 2로 고정되어있어 있다. 당연히 scale 이 2배로 커지는 동작만 실행된다. 누르고 있는 동안 2배, 2.5배, 3배.. 계속 커지도록 하려면 어떻게 해야 할까? 이벤트만 가지고는 이 동작을 수행할 수 없고 다른 도구의 도움을 얻어야 한다.
터치가 일어난다. 손이 떨어질 때까지, 그러니까 손이 떨어지지 않으면 버튼 크기를 조금씩 조금씩 크게 만들어야 한다. 여기서 버튼 크기를 크게 만드는 부분만 코드로 작성해 보자.
animate.ease(data.likeScale, data.likeScale.get() + 1)
현재 크기에서부터 조금 더 커져야 한다. 그래서 현재 값에 1을 더하는 형태로 코드를 작성했다. 그런데 data.likeScale + 1 이 아니라 data.likeScale.get() + 1 인 이유는 data 객체의 값들은 일반 값이 아니어서 사칙연산을 직접적으로 할 수 없도록 되어있다. 그래서 일반 값으로 변환해주는 get() 함수를 호출하여 더하기를 한 것이다. - 왜 이런 방식을 사용하는지는 기술적인 이유가 있지만 일단 지금 단계에선 그렇군! 하고 넘어가자 - onTapStart 함수 안의 코드를 이렇게 바꾼다 해서 결과가 달라지진 않는다. onTapStart 가 발생할 때 likeScale 값은 1이었고 거기에 1을 더해 2가 된 것이니 실제 이전 코드와 다를 바가 없기 때문이다.
그럼 어떻게? 그렇다 이 코드를 터치가 끝날 때까지 주기적으로 실행해줄 장치가 필요하다. 일정한 시간에 한 번씩 코드를 실행시켜주는 도구로 setInterval 함수가 제공된다. setInterval 함수의 첫 번째 인자로 실행할 코드를 담고 있는 함수를, 두 번째 인자로 반복 실행할 시간 주기를 밀리초 단위의 숫자를 넘겨준다. 밀리초는 1초를 1000으로 표현한다.
먼저 위 코드를 함수로 감싸 setInterval 함수가 호출할 수 있도록 형태를 만들다. 함수 이름은 계속 키운다는 의미로 keepGrowing이라 하겠다.
const keepGrowing = () => {
animate.ease(data.likeScale, data.likeScale.get() + 1)
}
export const KeepGrowingButton: Override = () => {
return {
scale: data.likeScale,
onTapStart() {
setInterval(keepGrowing, 100)
},
onTapEnd() {
animate.spring(data.likeScale, 1);
}
}
}
onTapStart에서 setInterval 함수를 이용하여 keepGrowing 함수를 0.1초에 한 번씩 keepGrowing 함수가 지속적으로 호출되도록 했다. 실행해 보면 손을 떼기 전까지 버튼이 계속 커지는 효과를 확인할 수 있다.
그러나 역시 문제가 발견된다. 손을 떼는 순간 잠깐 원래 크기로 돌아갔다가 순식간에 다시 커지기 시작하는 것이다. 왜냐하면 setInterval 은 그렇게 작동되는 함수이기 때문이다. onTapEnd에서 setInterval을 멈추게 할 방법이 필요하다. 방법은 여러 가지가 있는데 지난 연재의 예제에서 사용했던 간접 상태를 만들어 활용하는 방법을 사용해 보겠다.
먼저 버튼을 누르고 있는 상태인지 아닌지를 나타 내는 상태 하나를 만든다. 이름은, isGrowing!! 타입은 두 개의 값만 가지면 되니 true/false 값을 저장할 수 있는 boolean 타입으로 하고 초기값은 false로 한다.
let isGrowing = false
const data = Data({
likeScale: Animatable(1)
})
const keepGrowing = () => {
if (isGrowing) {
animate.ease(data.likeScale, data.likeScale.get() + 1)
}
}
export const KeepGrowingButton: Override = () => {
return {
scale: data.likeScale,
onTapStart() {
isGrowing = true
setInterval(keepGrowing, 100)
},
onTapEnd() {
isGrowing = false
animate.spring(data.likeScale, 1);
}
}
}
onTapStart에서 커져야 한다는 의미로 isGrowing을 true로 변경하고 setInterval을 시작한다. 0.1초에 한 번씩 호출되는 keepGrowing 함수 내에선 isGrowing 값이 true 인 경우에서 크기를 키우는 애니메이션을 호출한다. if (isGrowing === true) 이렇게 해도 되지만 비교해야 할 값이 boolean인 경우 if(isGrowing)처럼 간단히 작성할 수 있다. onTapEnd에서 isGrowing을 false로 변경하여 keepGrowing 함수가 더 이상 크기를 키우지 않도록 한다. 이제 우리가 원하는 동작대로 실행되는 것을 확인할 수 있을 것이다.
다음 연재에서 이 예제의 나머지 부분을 완성해 보도록 하겠다.