화이트큐브
경력근거: 사업 확장에 따른 채용이에요! 입사 지원 후, 사전 설문조사
챌린저스 글로벌셀은 여섯 명이 일본 진출 7개월 만에 월매출 7억과 MAU 15만을 만들었다고 적었고, 긴 기획보다 빠른 배포와 검증과 학습 사이클을 더 중요하게 본다고 Fast Iteration 을 못박았습니다. 우대사항에도 그로스 해킹과 A/B 테스트와 실험 기반 개발 경험이, 운영 자동화와 내부 툴 구축 경험과 나란히 걸려 있습니다. 사람이 적은 팀은 실험을 동시에 여러 개 켜게 되고 그때 같은 사용자가 두 실험에 함께 들어가면 결과를 믿을 수 없어 한 주가 통째로 날아가므로, 이 화면은 지표를 늘어놓는 대시보드가 아니라 실험을 켜기 전에 겹침을 사람이 해소하게 만드는 내부 툴로 잡았습니다. 이 포트폴리오는 결정론 해시 배정과 집합 연산 겹침 계산, 그리고 사람이 화면에서 위반을 해소해야만 다음이 진행되는 제품 흐름을 만드는 기술을 보여주며, 일본 뷰티 커머스 시장이나 그로스 실험 설계 도메인 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
글로벌팀 여섯 명이 일본 진출 7개월 만에 월매출 7억과 MAU 15만을 만들었고 속도와 실험과 실패를 용인하는 문화를 앞세운다고 적혀 있습니다. 사람이 적을수록 실험을 동시에 여러 개 켜게 되고, 그때 대상이 겹치는지 확인하는 자리가 필요해집니다.
우대사항에 그로스 해킹과 A/B 테스트와 실험 기반 개발 경험이 나란히 걸려 있습니다. 실험을 붙이는 기술보다 여러 실험을 동시에 굴릴 때 결과를 오염시키지 않는 배정 규칙이 실제로 어려운 부분입니다.
우대사항이 운영 자동화와 내부 툴 구축 경험을 함께 찾고, 반복적이고 비효율적인 작업을 빠르게 대체한다고 AI-first 를 적었습니다. 스프레드시트로 관리하던 실험 편성을 사람이 판단하는 자리만 남긴 화면으로 옮기는 일이 그 첫 조각입니다.
일본 앱에서 새 실험을 켜려는 사람이 지면과 대상 조건과 그룹별 트래픽 비율을 적으면, 지금 돌고 있는 실험과 대상이 몇 명 겹치는지와 같은 지면의 트래픽 합이 100 을 넘는지와 배정 salt 를 다른 실험이 이미 쓰고 있는지가 위반 칩으로 서는 사내 편성 화면입니다. 위반이 하나라도 남으면 시작 버튼이 잠긴 채로 있고, 사람이 비율을 줄이거나 두 실험을 상호배제로 묶거나 대상에서 겹치는 조건을 빼거나 기존 실험을 종료 예약하는 처리안 중 하나를 고르면 겹침 인원이 그 자리에서 다시 계산됩니다. 위반이 0 이 되어 확정하면 해시 버킷 경계까지 박힌 배정 규칙이 저장되고, 같은 규칙으로 앱 사용자 더미를 배정한 그룹별 인원과 다른 실험과 겹친 사용자 수가 함께 나옵니다.
챌린저스 일본 앱의 홈 배너와 초대 리워드를 매주 실험으로 바꿔 보는 글로벌셀 프로덕트 엔지니어입니다. 지금은 스프레드시트에 실험 대상과 트래픽 비율을 적어 두고 눈으로 겹침을 훑다가, 같은 신규 사용자가 배너 실험과 초대 실험에 동시에 들어간 것을 결과를 볼 때서야 알아채고 한 주를 버린 적이 있어, 켜기 전에 겹침을 인원으로 계산해 주는 화면을 원하는 사람입니다.
겹침 계산과 배정은 규칙으로 끝내되 남의 실험 트래픽을 줄이거나 실험을 상호배제로 묶는 결정만 사람이 화면에서 직접 고르게 만들면, 배정이 시작된 뒤에야 알아채던 대상 오염을 시작 전에 잡아 실험 한 주를 버리지 않게 된다는 가정입니다.
새 실험 초안을 지면과 대상 조건과 그룹별 트래픽 비율로 적으면 진행 중 실험과의 대상 겹침 인원과 지면 트래픽 합계 초과와 salt 재사용이 위반 칩으로 서고, 사람이 처리안을 고르기 전까지 시작 버튼이 잠긴 채로 있는 웹 화면이 배포까지 끝난 완결품입니다. 위반이 0 이 되어 확정하면 해시 버킷 경계까지 박힌 배정 규칙이 저장되고 앱 사용자 더미 5천 명을 그 규칙에 돌린 그룹별 인원과 겹친 사용자 수가 나옵니다. 실제 앱의 노출 로그 연동 대신 같은 모양의 사용자 더미를 담은 로컬 API 를 가정해 시작합니다. 만드는 덩어리는 편성 확정 화면과 배정 미리보기 둘이며 평일 5일에 혼자 끝나는 범위입니다.
시작한 실험의 노출과 전환을 그룹별로 집계하고 지금 트래픽으로 최소 표본에 언제 닿는지 날짜를 계산해 보여주는 결과 카드와, 실험 조합 40종의 겹침 인원을 집합 연산 정답과 대조해 계산을 보정한 2차 판입니다. 확정된 실험을 팀에 알릴 결과 메모 초안은 모델이 문장만 씁니다.
미국 지면을 추가해 시장별 트래픽을 따로 세고 JST 와 PST 경계에 걸친 기간을 어긋나지 않게 처리하는 확장판입니다. 배정 API 를 Docker 이미지로 묶어 AWS 에 올리고 Redis 로 사용자별 배정을 캐시해 앱이 매번 같은 그룹을 받는지 확인합니다.
새 실험 초안을 적어 위반 칩이 뜨고 트래픽 비율을 고쳐 위반이 0 이 된 뒤 배정 미리보기가 나오는 흐름을 처음부터 끝까지 시연합니다.
같은 지면 중복과 대상 겹침과 트래픽 합계 초과와 salt 재사용을 실험 더미에 심어 두고, 그 네 유형이 모두 위반으로 잡혀 시작 버튼이 잠기는 것과 사람이 고른 처리안이 배정 규칙에 그대로 반영되는 것을 보입니다. 다만 실험 더미와 정답을 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
user_id 대소문자가 흔들리고 locale 이 비어 있고 시장이 unknown 인 사용자 행 30종을 넣어도 배정이 멈추지 않고 처리되는 것을 보입니다.
실험 조합 40종을 넣어 화면이 계산한 겹침 인원이 집합 연산으로 구한 정답과 일치하는 비율을 2주차에 보고합니다. 규칙으로 채점하며 합성 기준 추정이라 실제 사용자 분포와 다를 수 있습니다.
위 수치는 더미데이터 자가채점이며, 실험 오염이 실제로 줄어드는지는 화이트큐브 일본 앱의 실제 사용자 분포와 담당자의 실사용이 있어야 측정 가능합니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태까지 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모가 도는 상태와 실검증 준비 상태로 솔직히 설명합니다.
정답 겹침 인원을 작성자가 만든 사용자 더미로 계산했으니 잘 맞는 것이 당연합니다. 대상 조건이 서로 포함 관계인 조합과 트래픽 합이 100 을 넘는 조합과 같은 salt 를 쓰는 조합과 조건이 아예 비어 전체 사용자를 받는 조합을 실험 더미에 일부러 섞어 두었습니다.
이 포트폴리오는 결정론 해시 배정과 집합 연산 겹침 계산, 그리고 사람이 화면에서 위반을 해소해야만 다음이 진행되는 제품 흐름을 만드는 기술을 보여주며, 일본 뷰티 커머스 시장이나 그로스 실험 설계 도메인 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
실제 도입에서는 지면 정의와 대상 조건 필드가 앱 버전마다 다르고 배정을 부르는 쪽이 서버인지 클라이언트인지에 따라 캐시 전략이 갈립니다. 지면 목록과 조건 필드를 실제 앱 스키마에 맞춰 다시 채우고 배정 호출 지점을 정하는 일을 도입 과제로 둡니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
새 실험을 켜려는 순간 이미 돌고 있거나 예약된 실험 목록입니다. 겹침 인원과 트래픽 합계와 salt 재사용을 판정하는 기준이 되는 입력입니다.
| exp_id | name_raw | surface | market | target_rule | traffic_pct | bucket_salt | status | starts_at | ends_at |
|---|---|---|---|---|---|---|---|---|---|
| EXP-101 | 홈 배너 딜 순서 A/B | home_banner | JP | {"first_open_days":"<=7"} | 50 | salt_home_a | running | 2026-07-20T00:00:00+09:00 | 2026-08-10T00:00:00+09:00 |
| EXP-102 | HOME-BANNER 신규 유저 딜 강조 | HOME-BANNER | JP | {"first_open_days":"<=7","is_purchaser":"false"} | 40 | salt_home_a | running | 2026-07-25T00:00:00+09:00 | — |
| EXP-103 | 초대 리워드 문구 | invite_sheet | JP | {} | 100 | salt_invite | running | 2026-07-15T00:00:00+09:00 | 2026-08-05T00:00:00+09:00 |
| EXP-104 | 딜 상세 가격 표기 | deal_detail | JP | {"is_purchaser":"true"} | 30 | salt_deal_p | paused | 2026-07-01T00:00:00+09:00 | 2026-07-31T00:00:00+09:00 |
| EXP-105 | 홈 배너 재방문 유도 | home_banner | JP | {"first_open_days":">30"} | 25 | salt_home_b | running | 2026-07-18T00:00:00+09:00 | 2026-08-08T00:00:00+09:00 |
아래 JSON Schema에 맞는 진행 중 실험 행을 40건 더 만들어 주세요. {"type":"object","properties":{"exp_id":{"type":"string"},"name_raw":{"type":"string"},"surface":{"type":"string"},"market":{"type":"string"},"target_rule":{"type":"object","additionalProperties":{"type":"string"}},"traffic_pct":{"type":"number"},"bucket_salt":{"type":"string"},"status":{"type":"string","enum":["running","paused","scheduled","ended"]},"starts_at":{"type":"string"},"ends_at":{"type":["string","null"]}},"required":["exp_id","name_raw","surface","market","target_rule","traffic_pct","bucket_salt","status","starts_at"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라. 첫째 같은 지면에 붙은 실험들의 traffic_pct 합이 100 을 넘는 조합, 둘째 target_rule 이 서로 포함 관계여서 대상이 통째로 겹치는 조합, 셋째 target_rule 이 빈 객체라 전체 사용자를 받는 실험, 넷째 같은 bucket_salt 를 두 실험이 함께 쓰는 조합, 다섯째 surface 를 home_banner 와 HOME-BANNER 와 home banner 처럼 다르게 적은 행, 여섯째 status 가 paused 나 ended 인데 ends_at 이 아직 미래이거나 null 인 행, 일곱째 market 이 JP 와 US 로 갈리면서 starts_at 의 시간대가 +09:00 과 -07:00 로 섞인 행. 실제로 켤 법한 지면 이름과 실험 이름을 쓰고 exp_id 는 EXP-숫자 형식을 지켜 주세요.{"type":"object","properties":{"exp_id":{"type":"string"},"name_raw":{"type":"string"},"surface":{"type":"string"},"market":{"type":"string"},"target_rule":{"type":"object","additionalProperties":{"type":"string"}},"traffic_pct":{"type":"number"},"bucket_salt":{"type":"string"},"status":{"type":"string","enum":["running","paused","scheduled","ended"]},"starts_at":{"type":"string"},"ends_at":{"type":["string","null"]}},"required":["exp_id","name_raw","surface","market","target_rule","traffic_pct","bucket_salt","status","starts_at"]}확정된 배정 규칙을 돌려 그룹별 인원과 다른 실험과 겹친 사용자 수를 세는 대상입니다. 겹침 인원의 정답을 집합 연산으로 계산하는 기준이기도 합니다.
| user_id | market | locale | signup_at | first_open_days | is_purchaser | invited_by |
|---|---|---|---|---|---|---|
| U-1001 | JP | ja-JP | 2026-07-28T11:20:00+09:00 | 3 | false | — |
| u-1001 | JP | ja-JP | 2026-07-28T11:20:00+09:00 | 3 | false | — |
| U-1002 | JP | ja-JP | 2026-07-19T23:59:00+09:00 | 12 | true | U-1001 |
| U-1003 | JP | 2026-07-20T00:01:00+09:00 | 11 | — | — | |
| U-1004 | unknown | en-US | 2026-06-02T08:40:00+09:00 | 59 | true | U-1002 |
아래 JSON Schema에 맞는 앱 사용자 행을 5000건 더 만들어 주세요. {"type":"object","properties":{"user_id":{"type":"string"},"market":{"type":"string"},"locale":{"type":"string"},"signup_at":{"type":"string"},"first_open_days":{"type":["integer","null"]},"is_purchaser":{"type":["boolean","null"]},"invited_by":{"type":["string","null"]}},"required":["user_id","market","locale","signup_at"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라. 첫째 같은 사람을 대소문자만 다르게 적은 user_id 쌍(해시 버킷이 갈리는지 확인하는 함정), 둘째 locale 이 빈 문자열이거나 ja_JP 처럼 구분자가 다른 행, 셋째 market 이 unknown 인 행, 넷째 first_open_days 와 is_purchaser 가 null 인 행, 다섯째 signup_at 이 자정 직전과 직후에 놓여 기간 조건 포함 여부가 갈리는 행, 여섯째 시간대가 +09:00 과 -07:00 으로 섞인 행, 일곱째 invited_by 가 존재하지 않는 user_id 를 가리키는 행. first_open_days 는 signup_at 과 어긋나지 않게 맞추고, 신규 사용자와 구매 이력 사용자의 비율은 3 대 7 정도로 두어 대상 조건이 서로 겹치는 구간이 실제로 생기게 만들어 주세요.{"type":"object","properties":{"user_id":{"type":"string"},"market":{"type":"string"},"locale":{"type":"string"},"signup_at":{"type":"string"},"first_open_days":{"type":["integer","null"]},"is_purchaser":{"type":["boolean","null"]},"invited_by":{"type":["string","null"]}},"required":["user_id","market","locale","signup_at"]}| TypeScript | 대상 조건과 배정 규칙 타입을 한 곳에 두고 편성 화면과 배정 엔진이 같은 정의를 씁니다. |
| React | 위반 칩과 트래픽 슬라이더와 처리안 선택을 상태로 묶어 값을 고칠 때마다 겹침 인원이 다시 계산되게 만듭니다. |
| Next.js | 실험을 켜는 사람이 여는 편성 화면과 초안을 검사하는 서버 라우트를 한 앱에 담습니다. |
| Python | 해시 버킷 배정과 대상 조건 두 개의 교집합 인원을 세는 집합 연산을 맡습니다. |
| FastAPI | 편성 초안을 받아 위반 목록과 배정 미리보기를 돌려주는 `POST /plan/validate` 와 확정을 저장하는 `POST /plan/commit` 을 냅니다. |
| PostgreSQL | 진행 중 실험과 확정된 배정 규칙의 단일 진실원입니다. |
| Supabase | PostgreSQL 을 호스팅하고 편성 화면을 여는 사내 담당자 로그인을 맡습니다. |
| Redis | 3주차에 사용자별 배정 결과를 캐시해 앱이 부를 때마다 같은 그룹이 나오는지 확인하는 자리에 씁니다. |
| Vercel | 1주차 편성 화면을 배포해 링크로 바로 열리게 만듭니다. |
| Docker | 3주차에 배정 API 를 이미지로 묶어 어디서 돌려도 같은 버킷 경계가 나오게 고정합니다. |
| AWS | 3주차에 그 이미지를 올려 앱 서버와 같은 망에서 배정을 부르게 만듭니다. |
| Claude | 2주차에 확정된 실험의 결과 메모 초안 문장만 씁니다. 인원과 비율은 규칙 계산이 냅니다. |
✓ pass → 여기서 멈춰도 실험을 켜는 사람이 혼자 쓰는 화면으로 완결됩니다. 더 키우려면 2주차에서 시작한 실험의 결과 카드를 붙입니다.
✓ pass → 계산이 흔들리지 않으면 3주차에서 미국 지면 추가와 배정 API 분리로 넓힙니다.
새 실험 초안 한 건의 편성 상태입니다. 위반마다 어느 실험과 몇 명이 겹치는지와 사람이 고른 처리안을 담고, 해소되지 않은 위반이 남아 있으면 배정 미리보기를 만들지 않아 시작이 잠깁니다.
{"type":"object","properties":{"plan_id":{"type":"string"},"draft":{"type":"object","properties":{"surface":{"type":"string"},"market":{"type":"string"},"target_rule":{"type":"object","additionalProperties":{"type":"string"}},"variants":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"traffic_pct":{"type":"number"}},"required":["key","traffic_pct"]},"minItems":2},"bucket_salt":{"type":"string"}},"required":["surface","market","target_rule","variants","bucket_salt"]},"conflicts":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","enum":["audience_overlap","traffic_over","salt_reuse","surface_alias","sample_short"]},"with_exp":{"type":["string","null"]},"detail":{"type":"string"},"affected_users":{"type":"integer"},"resolution":{"type":["string","null"],"enum":["reduce_traffic","mutually_exclusive","exclude_rule","end_other",null]}},"required":["code","detail","affected_users","resolution"]}},"blocking_count":{"type":"integer"},"assignment_preview":{"type":["object","null"],"properties":{"group_counts":{"type":"object","additionalProperties":{"type":"integer"}},"overlap_users":{"type":"integer"}}},"hedge":{"type":["string","null"]}},"required":["plan_id","draft","conflicts","blocking_count"]}[
{
"plan_id": "PL-001",
"draft": {
"surface": "home_banner",
"market": "JP",
"target_rule": {
"first_open_days": "<=7"
},
"variants": [
{
"key": "control",
"traffic_pct": 15
},
{
"key": "b",
"traffic_pct": 15
}
],
"bucket_salt": "salt_home_a"
},
"conflicts": [
{
"code": "audience_overlap",
"with_exp": "EXP-102",
"detail": "첫 실행 7일 이내 사용자가 통째로 겹칩니다.",
"affected_users": 1284,
"resolution": null
},
{
"code": "traffic_over",
"with_exp": null,
"detail": "home_banner 지면의 트래픽 합이 195 퍼센트입니다.",
"affected_users": 0,
"resolution": null
},
{
"code": "salt_reuse",
"with_exp": "EXP-101",
"detail": "같은 salt 를 써서 두 실험의 버킷 경계가 붙습니다.",
"affected_users": 0,
"resolution": null
}
],
"blocking_count": 3,
"assignment_preview": null,
"hedge": "위반이 남아 있어 배정을 만들지 않고 시작을 잠급니다. 인원은 합성 사용자 기준입니다."
},
{
"plan_id": "PL-002",
"draft": {
"surface": "home_banner",
"market": "JP",
"target_rule": {
"first_open_days": "<=7",
"is_purchaser": "false"
},
"variants": [
{
"key": "control",
"traffic_pct": 10
},
{
"key": "b",
"traffic_pct": 10
}
],
"bucket_salt": "salt_home_e"
},
"conflicts": [
{
"code": "audience_overlap",
"with_exp": "EXP-102",
"detail": "겹치는 사용자를 상호배제로 묶어 한쪽에만 들어가게 했습니다.",
"affected_users": 1284,
"resolution": "mutually_exclusive"
},
{
"code": "traffic_over",
"with_exp": null,
"detail": "EXP-111 의 비율을 45 에서 25 로 줄여 합을 100 이하로 맞췄습니다.",
"affected_users": 0,
"resolution": "reduce_traffic"
}
],
"blocking_count": 0,
"assignment_preview": {
"group_counts": {
"control": 486,
"b": 491,
"excluded": 4023
},
"overlap_users": 0
},
"hedge": "그룹 인원은 합성 사용자 5천 명을 배정한 결과라 실제 트래픽과 다릅니다."
},
{
"plan_id": "PL-003",
"draft": {
"surface": "invite_sheet",
"market": "JP",
"target_rule": {},
"variants": [
{
"key": "control",
"traffic_pct": 50
},
{
"key": "b",
"traffic_pct": 50
}
],
"bucket_salt": "salt_invite"
},
"conflicts": [
{
"code": "audience_overlap",
"with_exp": "EXP-103",
"detail": "대상 조건이 비어 있어 전체 사용자가 겹칩니다.",
"affected_users": 5000,
"resolution": null
},
{
"code": "sample_short",
"with_exp": null,
"detail": "초대 시트 노출이 하루 900 회라 최소 표본 4000 에 닿는 데 9 일이 걸립니다.",
"affected_users": 0,
"resolution": null
}
],
"blocking_count": 2,
"assignment_preview": null,
"hedge": "노출 수와 최소 표본은 합성 기준 추정이라 실제 일 트래픽과 다릅니다."
}
]code 와 resolution 의 허용 값, variants 최소 2건, 필수 필드는 스키마로 자동 검증합니다. blocking_count 가 resolution 이 null 인 conflicts 수와 같은지, blocking_count 가 0 보다 크면 assignment_preview 가 비어 있는지, affected_users 가 두 target_rule 을 사용자 더미에 돌린 교집합 크기와 같은지, 같은 user_id 와 salt 로 다시 배정해도 같은 variant 가 나오는지는 규칙 검사기가 확인합니다. 채점이 집합 연산과 해시 재현성으로 끝나 사람 판단 없이 점수가 나옵니다.