-
Notifications
You must be signed in to change notification settings - Fork 0
카드 게임 로딩 상태 심리스하게 보여주기
저희는 선착순 이벤트로 카드 게임이라는 포맷을 채택하고 있어요. 저희가 카드 게임의 포맷을 채택한 이유는 다음과 같아요.
- 선착순 이벤트의 목적은 이벤트 기간동안 지속적인 트래픽을 유도하는 것이에요. 따라서, 누구나 부담없이 참여할 수 있는 형식의 인터랙션을 채택해서, 사용자를 꾸준히 웹페이지에 유입시키도록 구성했어요.
- 선착순 이벤트에 '운'의 요소를 가미해서 재미를 주고자 했어요.
(그래프 들어감)
카드 게임의 정답은 서버에서 관리하고 있어요. 사용자가 카드 게임에서 카드를 클릭하면 다음과 같은 플로우로 내부적으로 동작해요.
- 서버로 클릭한 카드의 인덱스를 제출하여, 카드 게임의 정답과 당첨 여부를 요청해요.
- 서버는 사용자가 제출한 카드의 인덱스와 서버가 갖고 있는 카드 게임의 정답을 비교해요. 이후, 정답이면 사용자를 선착순 이벤트에 응모를 완료시켜요.
- 서버가 클릭한 카드의 정답 여부와, 선착순 이벤트 당첨 여부를 반환해요.
- 서버가 반환한 정답 여부에 따라, 클라이언트가 뒷면의 그림을 변화시킨 뒤, 카드를 뒤집어요.
보는 바와 같이, 저희는 서버에서 정답 여부의 응답이 도착하는 시점에 카드를 뒤집고, 뒷면의 그림을 변화시키도록 구성했어요. 만약 뒷면의 그림을 미리 변화시킨다면, 악의적인 사용자가 HTML을 뜯어서 미리 정답을 알아내는 문제가 생길 수 있다고 생각했고, 카드를 먼저 뒤집고 정답 여부가 확정될 때 카드의 그림이 변화된다면 네트워크가 느린 사람의 경우 카드의 모양이 중간에 바뀌어서 어색함을 느끼게 될 것이라고 생각했어요. 매번 서버로 정답 여부를 판정한 뒤, 응답이 도착하는 시점에 카드를 뒤집는 것은 카드 게임의 정답을 온전히 서버가 관리하도록 할 수 있고, 클라이언트는 정답의 여부를 몰라도 되기 때문에 보안상 이점이 있으며, 뒷면의 그림을 변화시키면서 동시에 카드를 뒤집기 때문에, 뒤집어진 카드의 그림이 변화되는 어색함을 해소할 수 있다고 생각했어요.
하지만, 만약 네트워크가 느린 사용자라면, 서버에 존재하는 답을 알아내는 데 오랜 시간이 걸리기 때문에, 카드를 클릭한 시점부터 실제로 카드가 뒤집히는 시점까지 오랜 시간을 기다려야 할 거에요. 사용자가 어떠한 행동을 하면, 그 행동에 대해 즉각적인 피드백을 주어야 사용자 경험을 향상시킬 수 있지만, 이 경우는 완전히 대치되는 문제였어요. 그래서 저희는 어떻게 네트워크가 느린 사용자에게 즉각적인 반응을 제공하면서도, 로딩 중이라는 느낌을 덜 살리고 사용자의 텐션을 늘릴 수 있을지 고민했어요.
카드 게임의 정답은 프론트엔드에서 관리할 수도 있고, 백엔드에서 관리할 수도 있어요. 두 방법의 장단점은 다음과 같아요.
- 프론트엔드 관리 : 백엔드에서 연산하는 부하를 줄일 수 있으며, 랜덤으로 관리되는 정답보다 선착순 자체의 순위가 중요해서 크리티컬하지 않은 것처럼 보이지만, 어떤 악의적인 사람이 자바스크립트의 코드를 뜯어서 변수를 모니터링해서 선착순 이벤트에 대한 이득을 가져갈 수 있다는 보안상 오류가 있었어요.
- 백엔드 관리 : 보안상 이점을 얻을 수 있지만, 완벽한 보안을 위해서라면 카드의 정답을 얻기 위해 매번 API를 호출해야 한다는 문제가 있었어요.
저희 팀의 논의 결과, 카드 게임의 정답을 백엔드에서 관리하는 것이 좋다고 생각했어요. 그 이유는 카드 게임의 정답이 공정성이 요구되는 크리티컬한 정보라고 간주했기 때문이에요. 만약 악의적인 사용자가 클라이언트의 코드를 조작해서 카드 게임의 정답을 알게 된다면, 선착순 이벤트에서 절대적인 우위를 가지게 된다고 생각했어요. 그렇게 된다면, 이벤트의 공정성이 파괴되고, 이벤트와 서비스의 신뢰성이 떨어지는 문제가 생길 수 있을 것이라고 생각했어요.
비동기 요청에는 4가지 상태가 존재해요. 인터랙션 전, 로딩, 완료, 오류가 그것이에요. 여기에서는 '오류' 상태를 제거하고, 인터랙션 전, 로딩, 완료라는 3가지 상태만 다루도록 해요. 저희의 카드 게임 인터랙션을 3가지 단계로 분류하자면, 다음과 같다고 할 수 있어요.
- 인터랙션 전 : 카드가 뒤집혀진 상태로 있음. 뒷면은 당첨 실패 이미지(디폴트)가 존재함.
- 로딩 : (무언가의 시각적 변화)
- 완료 : 카드가 180도로 뒤집어지는 애니메이션이 재생되고, 만약 뒤집은 카드가 정답이라면, 뒷면이 당첨 성공 상태가 됨.
(시청각 자료-그래프)
가장 간단한 건 카드가 로딩 중인 상태에 로딩 인디케이터를 보여주는 것이에요. 이렇게 하면 로딩 중에도 무언가의 변화가 존재하기 때문에, 즉각적인 피드백을 볼 수 있어요. 하지만, 네트워크가 느린 소수의 사용자를 제외한 다수의 네트워크가 빠른 사용자는, 로딩 인디케이터가 순간적으로 생겼다가 순간적으로 사라지는 어색함을 겪게 될 거에요.
저희는 다음의 고려사항을 주제로, 토의를 거쳤어요.
- 클릭하자마자 즉각적인 피드백을 제공하는가?
- 네트워크가 빠른 사용자(0.2s 이하) 가 로딩 중 상태에서 완료 상태로 전환될 때, 어색하지 않은가?
- 카드 뽑기 게임의 텐션을 상승시킬 수 있을 만큼 적절한가?
그리고, 다음의 2가지 아이디어를 도출했어요.
- 카드가 빛나면서, 가챠 게임처럼 흔들리는 연출.
- 카드가 위로 살짝 올라가는 연출.
그 중, 저희는 첫 번째 아이디어를 채택했어요. 이유는 다음과 같아요.
- 카드가 빛나는 애니메이션은 즉각적으로 확인할 수 있다는 이점이 있어요.
- 만약 네트워크가 빠른 사용자라고 해도 카드의 모양은 그대로이기 때문에, 로딩 상태에서 완료 상태로 변경되었을 때 어색함이 줄어들어요.
- 카드가 흔들리는 애니메이션은 카드의 상태에 더 쉽게 변화를 줄 수 있기 때문에, 확연한 변화가 존재한다고 사용자가 인지할 수 있어요.
- 이 때, 카드가 흔들리는 애니메이션은 네트워크가 느린 사용자의 경계인 0.3s 이후에 이루어지기 때문에 네트워크가 빠른 사용자도 자연스럽게 완료 애니메이션을 볼 수 있어요.
- 만약 카드가 흔들리더라도, 빠르게 흔들리기 때문에 어느 시점에서라도 카드가 원상태로 돌아올 때 자연스러움을 느낄 수 있어요.
- 실제 가챠 게임처럼, 빛나는 시각 효과를 줌으로써 사용자가 기다리면서 텐션을 끌어올릴 수 있어요.
.card {
transform-style: preserve-3d;
}
.card.flipped {
transform: rotateY(180deg);
}
.front {
transition: filter 0.1s ease-out;
}
.pending {
animation: 0.2s linear 0.3s infinite shake;
}
.pending .front {
filter: brightness(2.9) drop-shadow(0px 0px 40px white);
transition: filter 0.8s ease-out;
}
.back {
transform: translateZ(-0.1px) rotateY(180deg);
}
@keyframes shake {
0% {
transform: rotate(0deg);
}
10% {
transform: rotate(5deg);
}
30% {
transform: rotate(-3deg);
}
50% {
transform: rotate(6deg);
}
70% {
transform: rotate(-5deg);
}
90% {
transform: rotate(4deg);
}
100% {
transform: rotate(0deg);
}
}
<button
className={`${style.card} ${isPending && !isFlipped ? style.pending : ""} ${isFlipped ? style.flipped : ""}`}
onClick={flip}
onTransitionEnd={ ()=>setGlobalLock(false) }
disabled={locked || fliped || isFlipped}
>
<div className={`${cardFaceBaseStyle} ${style.front}`}>
<img src={hidden1x} srcSet={`${hidden1x} 1x, ${hidden2x} 2x`} alt="hidden" className="w-full h-full" draggable="false" />
</div>
<div className={`${cardFaceBaseStyle} ${style.back}`}>
<img src={answer1x} srcSet={`${answer1x} 1x, ${answer2x} 2x`}
alt={isCorrect ? "축하합니다, 당첨입니다!" : "아쉽게도 정답이 아니네요!"}
className="w-full h-full" draggable="false" />
</div>
</div>
우선, 기다리는 상태일 때, shake 애니메이션을 0.2초간 무한반복하게 만들었어요. 이 애니메이션은 pending 상태가 된 0.3초 뒤에 실행되며, 대상인 카드를 빠르게 작게 회전하면서 떨림 효과를 주는 애니메이션이에요. 그리고, 밝아지면서 글로우 효과를 주는 애니메이션을 카드의 앞면에 추가했어요. 카드 자체에 추가하지 않은 까닭은 애니메이션과 트랜지션이 별개로 동작해야 하기 때문이에요. 카드의 글로우 효과는 카드가 pending 상태일 때 0.8초 동안 재생되게 하여 서서히 재생하게 했고, pending 상태에서 벗어나면 0.1초 동안 원상태로 되돌아가게 했어요. 이렇게 한 까닭은 네트워크가 빠른 사용자의 찰나의 순간 진행되는 로딩 상태에서 글로우 효과의 양을 최대한 줄여, 네트워크가 빠른 사용자에게도 글로우 효과가 나타나다 사라지는 것이 어색하지 않도록 했어요.
문제는 이렇게 구현했을 때, 카드가 뒤집히는 애니메이션이 동작하지 않는다는 것에 있어요. 이 문제는 애니메이션을 일으키는 css 클래스가 트랜지션을 일으키는 css 클래스로 전환될 때, 트랜지션이 일어나지 않는다는 현상에서 비롯되어요.
Note that above rules mean that transitions do not start when the computed value of a property changes as a result of declarative animation (as opposed to scripted animation). This happens because the before-change style includes up-to-date style for declarative animations. 위의 규칙은 속성의 계산 값이 (스크립트 애니메이션과는 반대로) 선언 애니메이션의 결과로 변경될 때 전환이 시작되지 않는다는 것을 의미합니다. 이는 변경 전 스타일에 선언 애니메이션에 대한 최신 스타일이 포함되어 있기 때문에 발생합니다.
이 명세를 알기 쉽게 풀어서 설명하자면, css의 트랜지션은 이전 스타일과 현재 스타일이 다르다고 판별되었을 때 발생하는데, css의 keyframes 애니메이션으로 발생하는 애니메이션은 애니메이션 도중 이전 스타일이 곧 현재 스타일이므로, 애니메이션을 종료하고 트랜지션을 시작할 때 트랜지션을 일으킬 필요가 없다고 판별한다는 의미에요.
<button className={`${style.card} ${isFlipped ? style.flipped : ""}`}>
<div className={`${cardFaceBaseStyle} ${isPending && !isFlipped ? style.pending : ""}`}>
<img src="/앞면.png" className={`${cardFaceBaseStyle} ${style.front}`}/>
<img src="/뒷면.png" />
</div>
</button>
저희는 이 문제를 pending 애니메이션을 일으키는 요소와 실제로 카드를 뒤집는 트랜지션을 일으키는 요소를 다르게 해서 해결했어요. 이렇게 하면 같은 태그에 애니메이션과 트랜지션의 충돌이 일어나지 않아서, 애니메이션이 개별적으로 종료되고, 트랜지션이 개별적으로 시작되어서 정상적으로 애니메이션을 종료한 뒤 뒤집는 트랜지션을 적용시킬 수 있어요.
-
🎯 기술적 선택 이유
-
✨ UX 및 접근성
-
#️⃣ 코드 퀄리티
-
🛠️ 구현