상황
출석 챌린지에는 PC 팝스킨과 웹 현황판이 있습니다. 팝스킨은 출석 같은 행동을 안내하는 작은 창이고, 현황판에서는 출석 기록과 리워드를 확인합니다. 7일을 채우면 광고 제거 리워드를 받습니다. 출석 후 4시간이 지나면 자동 출석권도 획득할 수 있습니다. 자동 출석권은 빠진 날의 출석을 채우는 데 쓰입니다.
처음에는 서버에서 팝스킨을 선택하려고 todayStatus를 만들었습니다. PC에서 API를 호출하면 서버가 값을 정하고, PC는 그 값에 맞는 팝스킨을 띄우는 흐름이었습니다.
todayStatus에는 출석 가능, 자동 출석권 획득 대기, 획득 가능, 오늘 완료를 나타내는 값이 있었습니다. 출석 안내와 획득 안내를 띄울지 정하는 데는 이 구분으로 충분했습니다.

리워드 안내까지 보여 주면서 다른 값도 확인해야 했습니다. 아직 리워드를 받지 않았다면 수령 안내가 필요했습니다. 리워드를 받으면 광고 제거가 시작되고, 광고 제거가 적용된 상태에서 다시 완주하면 연장을 기다립니다. 클라이언트는 todayStatus에 수령 기한과 광고 제거 종료일을 조합해 화면을 골랐습니다.
문제
자동 출석권은 획득한 주차에서만 사용할 수 있었습니다. 기존 정책에서는 완주 후 리워드를 받은 날에 자동 출석권을 더 획득할 수 없었습니다.
사용 기한을 없애면서 이 규칙도 바뀌었습니다. 이제 리워드를 받은 날에도 조건을 충족하면 자동 출석권을 획득할 수 있습니다. 광고 제거 연장을 기다리는 동안에도 마찬가지입니다. 연장 안내와 획득 안내가 함께 필요한 상황이 생겼습니다.

todayStatus만으로 어떤 화면을 띄울지 정하기가 어려워졌습니다.
BONUS_AVAILABLE이어도 리워드를 받은 날인지, 연장을 기다리는 중인지에 따라 웹 현황판의 안내가 달랐습니다.DONE인 경우에도 받을 리워드가 남아 있으면 수령 안내를 보여 줘야 했습니다.
응답 값을 더 조합하면 화면을 고를 수는 있었습니다. 다만 정책을 바꿀 때마다 PC와 웹의 조건을 함께 확인해야 했습니다. 서버에서 화면을 선택하려던 의도와 달리, 선택 규칙을 클라이언트에서도 관리하고 있었습니다.
원인
출석과 리워드를 한 줄의 진행 단계로 생각했던 것이 문제였습니다. 오늘 출석했는지와 광고 제거가 적용 중인지는 따로 확인해야 합니다. 출석 여부는 자정을 기준으로 달라지고, 리워드는 날짜가 바뀌어도 이어집니다.
Harel statechart로 상태의 관계를 살펴봤습니다
Harel의 Statecharts: A Visual Formalism for Complex Systems는 1987년에 발표된 논문입니다. 이벤트에 반응하는 시스템의 동작을 표현하기 위해 기존 Finite State Machine을 확장했습니다.
평평한 상태 목록에서는 시스템의 전체 상태를 하나씩 나열합니다. 서로 다른 기능의 조합까지 나열하면 상태와 전이가 많아집니다. Statechart에서는 관련 상태를 묶고, 동시에 유지되는 상태는 영역을 나눠 표현합니다. 논문에서 다루는 개념을 다음처럼 정리했습니다.
| 개념 | 표현하는 관계 | 그림에서 읽는 법 |
|---|---|---|
hierarchy | 상태 안에 하위 상태를 둡니다 | OR-state 안에서는 하위 상태 하나가 활성화됩니다. 바깥 상태도 함께 활성화됩니다 |
orthogonality | 여러 영역의 상태를 동시에 유지합니다 | AND-state를 점선으로 나누고, 각 영역의 활성 상태를 함께 읽습니다 |
communication | 영역 사이의 이벤트와 조건을 연결합니다 | 다른 영역의 상태를 전이 조건으로 확인하거나, 발생한 이벤트를 여러 영역에서 처리할 수 있습니다 |
예를 들어 두 영역에 각각 상태가 2개와 3개 있다면 조합은 최대 6개입니다. 영역을 나누면 각 상태를 2개와 3개로 따로 그릴 수 있습니다. 표현이 간결해져도 가능한 조합과 전이 조건은 여전히 확인해야 합니다.
출석과 리워드에 적용해 봤습니다
이 관점으로 출석 챌린지를 다시 나눴습니다.
- 하루 영역에서는 ‘출석 전’과 ‘출석함’을 구분했습니다. 출석해야 자동 출석권 획득을 기다릴 수 있으므로, ‘출석함’ 안에 획득 대기·가능·완료·불가를 뒀습니다. 이 관계를
hierarchy로 표현했습니다. - 리워드의 수령 대기와 연장 대기는 별도의
orthogonal region에 뒀습니다. 오늘의 획득 상태와 리워드 상태를 함께 읽으면 ‘연장 대기이면서 획득 가능’한 조합을 표현할 수 있습니다.
그림에서는 두 영역의 관계를 보기 위해 리워드 적용 여부와 세부 전이는 생략했습니다.

영역을 나눠도 행동 가능 여부를 판정할 때는 함께 확인합니다. 다른 영역의 상태를 전이 조건으로 사용하는 방식입니다.
- 리워드 수령 대기 중에는 출석과 자동 출석권 획득이 막힙니다.
- 연장 대기 중에는 그날의 자동 출석권을 획득할 수 있습니다.
Statechart로 상태의 관계를 정리한 뒤, 함께 성립하는 조합을 화면 정의와 비교했습니다. 각 조합에서 어떤 화면을 보여 줄지는 별도의 선택 규칙으로 정해야 했습니다.
해결
화면 유형을 서버에서 정했습니다
화면 정의서를 펼쳐 보니 노출할 화면이 30개를 넘었습니다. 각 클라이언트에서 전달받은 유형 하나에 화면 하나가 대응하도록 정하고 싶었습니다. 이를 위해 상태 조합과 노출할 화면을 표로 정리했습니다.
예를 들어 연장을 기다리는 중에도 자동 출석권을 획득할 수 있다면, PC와 웹에서는 다음과 같이 보여 줍니다.
- PC에서는 자동 출석권 획득 팝스킨을 띄웁니다.
- 웹 현황판에서는 연장 안내와 획득 버튼을 함께 보여 줍니다.
같은 상태에서도 PC와 웹이 보여 줄 내용은 달랐습니다.

표를 기준으로 서버에 화면 선택 규칙을 작성했습니다. 출석과 자동 출석권 획득 가능 여부는 공통으로 판정하고, 리워드 상태를 조합해 PC 팝스킨과 웹 현황판의 유형을 각각 골랐습니다. 출석이나 수령을 마친 뒤 띄울 결과 알림도 서버에서 정했습니다.
| 응답 값 | 클라이언트에서 보여 줄 것 |
|---|---|
popupType | PC 팝스킨 |
boardType | 웹 현황판 |
noticeType | 행동 직후 결과 알림 |
응답에는 선택한 유형과 화면에 표시할 숫자·날짜를 담았습니다. 화면 선택에 쓰던 todayStatus와 rewardPending은 응답에서 뺐습니다. PC와 웹에서 출석과 리워드 조건을 다시 조합하던 코드를 걷어낼 수 있도록 계약을 바꿨습니다.
화면 유형과 선택 규칙을 함께 관리했습니다
코드에서는 정보를 읽고, 가능한 행동을 판정한 뒤 화면을 고르도록 역할을 나눴습니다.
ChallengeState에는 사용자의 출석 기록과 리워드 정보를 담았습니다.TodayStatus에서는 이 정보로 오늘 출석하거나 자동 출석권을 획득할 수 있는지 판정합니다. 출석 기록 처리에도 필요하므로 서버 내부에 남겼습니다.PopupType과BoardType에서는 화면을 선택합니다. 각 enum의of메서드에서TodayStatus와 리워드 상태를 확인하고 유형을 반환합니다.
화면 선택 코드에는 조건을 확인하는 순서도 적었습니다. 예를 들어 PC 팝스킨은 오늘 할 수 있는 행동보다 리워드 수령 대기를 먼저 확인합니다.
이 구조를 정하면서 유형을 코드의 enum으로 관리할지, DB에 저장할지도 비교했습니다.
| 관리 방식 | 장점 | 비용 |
|---|---|---|
| enum과 코드 | 유형과 선택 규칙을 함께 수정하고 리뷰·테스트할 수 있습니다 | 유형을 바꾸면 서버를 다시 배포해야 합니다 |
| DB | 코드가 지원하는 값은 배포 없이 수정할 수 있습니다 | 데이터 변경도 검증해야 하며, 새 규칙은 코드 수정이 필요할 수 있습니다 |
이번에는 유형마다 출석과 리워드 조건을 조합하는 규칙이 복잡했습니다. 유형 이름만 DB로 옮겨도 선택 규칙은 코드에 남고, 규칙까지 옮기려면 조건을 저장하고 해석하는 방식이 필요했습니다.
현재는 함께 바뀌는 유형과 선택 규칙을 같은 enum에 두는 편이 낫다고 판단했습니다. 변경할 때 관련 조건과 순서를 한곳에서 확인하고, 조합별 결과를 테스트할 수 있기 때문입니다. 문구나 노출 기간처럼 정해진 규칙 안의 값만 자주 바뀐다면, 그 값은 DB에서 관리하는 방식을 고려할 수 있습니다.
결과
서버에서 상태 조합을 확인하고, PC와 웹에 전달할 화면 유형을 정하도록 바꿨습니다. 팝스킨 하나를 띄우기 위해 클라이언트에서 여러 응답 값을 해석하던 규칙을 서버로 옮겼습니다.

클라이언트에서 처리할 조건도 남았습니다.
- PC에서는 노출 횟수와 유료 라이선스에 따른 팝스킨 숨김을 처리합니다.
- 로딩과 오류 안내는 각 클라이언트에서 처리합니다. 웹의 이벤트 종료 화면 분기도 남겨 뒀습니다.
기존 유형으로 표현할 수 있는 조합은 서버의 선택 규칙을 수정하면 됩니다. 새 화면 유형이 필요하면 서버와 클라이언트를 함께 수정해야 합니다. 이번 변경도 기존 응답 필드를 제거했기 때문에 함께 배포해야 합니다.
처음에는 todayStatus에 값을 더 넣으면 될 줄 알았습니다. 상태를 나누고 화면별로 비교해 보면서, 어디에서 화면을 선택할지까지 정해야 한다는 것을 알게 됐습니다.