기술 정보

이 사이트가 어떻게 만들어졌고 무엇을 저장하며, 어떤 숫자가 실측이고 어떤 숫자가 본 사이트의 추산인지 설명합니다.

렌더링

Next.js 15 App Router, React 19, strict 모드 TypeScript를 사용합니다. 스타일은 Tailwind v4로, 팔레트는 단일 스타일시트에 테마 토큰으로 선언되어 있습니다. 컴포넌트 라이브러리도 차트 라이브러리도 없습니다. 다섯 가지 차트는 모두 손으로 작성한 SVG라 서버에서 렌더링되고 테마 변수를 상속합니다 — 라이트 모드가 런타임 비용 없이 동작하는 이유이기도 합니다.

로케일 세그먼트 아래의 모든 페이지는 정적 생성이 아니라 요청마다 렌더링됩니다. 이는 실수가 아니라 의도된 선택입니다. 헤더가 세션별 상태(로그인한 사용자와 그 북마크)를 표시하므로 로케일 레이아웃이 dynamic = "force-dynamic" 을 설정해 하위 트리 전체가 정적 생성에서 빠집니다. 로케일 자체는 URL에 있으며, middleware가 쿠키, 다음으로 Accept-Language 에서 로케일을 협상하고 선택 결과를 요청 헤더에 복사해 루트 레이아웃이 <html lang> 을 설정할 수 있게 합니다.

페이지가 공식 API를 직접 호출하는 일은 없습니다. 단일 서비스 모듈을 호출하며, 그 모듈이 요청마다 실시간 API·로컬 캐시·토큰 미설정 시 결정적 데모 데이터 중 무엇을 반환할지 결정하고, 그 판단을 데이터와 함께 돌려주어 UI가 표시할 수 있게 합니다.

로컬 색인

영속화는 Node 22부터 내장된 SQLite 바인딩인 node:sqlite 입니다. 컴파일할 네이티브 모듈도, 운영할 데이터베이스 서버도, ORM도 없습니다 — 데이터 계층 전체가 SQL 파일 하나와 소수의 작은 타입 지정 헬퍼입니다. 파일은 DATABASE_PATH 가 가리키는 위치(기본값 ./data/brawlinsights.db)에 있으며, WAL 저널링, 외래 키 활성화, 5초 busy timeout으로 열립니다.

스키마는 시작할 때마다 CREATE TABLE IF NOT EXISTS 로 생성됩니다. 이는 기존 테이블에 컬럼을 더하지 않으므로, 새 필드는 각 테이블을 검사해 누락된 ALTER TABLE 을 실행하는 별도 단계가 적용합니다. 덕분에 기존 설치본은 버려지지 않고 새 컬럼을 얻습니다.

공식 API가 주지 않는 쪽의 데이터가 여기에 저장됩니다:

  • 이름 검색을 가능하게 하는 플레이어·클럽 디렉터리;
  • 폴링 사이의 변화를 포착해 재구성한 이름 기록과 클럽 기록;
  • 추이 차트의 원천인 플레이어별 매시간 진행 스냅숏;
  • 보관된 전투 기록과 그로부터 집계한 일별 통계;
  • 장착 스킨, 그리고 관측된 프로필에서 학습한 경쟁전 티어 라벨 표;
  • 그리고 사이트가 직접 책임지는 것 전부 — 계정과 거기에 딸린 세션, 북마크, 조회 기록, 업로드한 이미지, 채팅과 다이렉트 메시지, 게시판 글, 버그 신고, 모더레이션 기록. 전체 목록은 개인정보처리방침 2절이 스키마에서 생성하므로 여기서 반복하지 않습니다.

공식 API와의 통신

하나의 모듈이 api.brawlstars.com/v1 을 감쌉니다. 모든 응답은 엔드포인트별 재검증 창과 캐시 태그를 붙여 Next의 데이터 캐시에도 보관되며, 플레이어·클럽 프로필은 짧은 TTL 동안 SQLite 사본을 먼저 재사용한 뒤에야 새 요청을 보냅니다. 404 이외의 이유로 실패하면 페이지를 오류로 만들지 않고 마지막 저장 사본을 제공합니다. 404는 계정이 사라졌다는 뜻이므로 페이지는 찾을 수 없음을 보고하고, 갱신 큐가 그 태그를 색인에서 제거합니다.

요청재검증 간격
플레이어 프로필120 s
전투 기록60 s
클럽, 클럽 멤버300 s
랭킹900 s
이벤트 로테이션600 s
브롤러 로스터86400 s

API 토큰은 요청을 보내는 공인 IP에 묶이므로 403 accessDenied.invalidIp 가 프로덕션에서 단연 가장 흔한 실패입니다. 이는 별도로 감지되어 일반 오류로 기록되는 대신 상태 엔드포인트에 알기 쉬운 힌트와 함께 표시됩니다.

갱신 큐

API에는 푸시 채널도 「특정 시점 이후 변경」 필터도 없어, 최신 상태를 유지하는 유일한 방법은 폴링입니다 — 그런데 모든 것을 똑같이 폴링하면 느리고 낭비입니다. 그래서 색인된 모든 플레이어는 우선순위 밴드에 속하고, 레코드는 그 밴드가 허용하는 나이보다 오래되어야 갱신 후보가 됩니다. 후보는 높은 밴드 먼저, 오래된 레코드 먼저 처리됩니다.

hot어느 계정이든 북마크했거나, 최근 1시간 안에 조회됨2 분
warm최근 하루 안에 조회됨15 분
cold색인된 적 있는 그 밖의 모든 계정6 시간

이 프로필 패스에서 전투 기록은 hot과 warm 밴드에서만 프로필과 함께 가져옵니다 — 콜드 계정은 마지막으로 본 이후 플레이하지 않았을 가능성이 높고, 기록 하나가 요청 하나를 더 쓰기 때문입니다. 클럽은 warm 기준으로 자체 스윕을 가지며 북마크된 클럽이 먼저입니다. 플레이어를 갱신하면 그 플레이어의 클럽도 갱신되어, 세 번째 큐 없이도 클럽 페이지가 최신으로 유지됩니다.

다만 전투 기록은 이 밴드에 갇히지 않습니다. 별도의 수확 패스 — 보통 각 사이클에서 가장 큰 몫 — 가 각 계정의 기록을 마지막으로 수집한 지 얼마나 됐는지 순으로 색인 전체(콜드 계정 포함)를 돌며, HARVEST_INTERVAL(30 분) 안에 수확되지 않은 모든 계정의 기록을 가져옵니다. 아무도 조회하지 않는 계정도 경기를 계속하며, 그 경기가 티어 리스트 표본을 키웁니다 — 코퍼스를 만드는 것은 프로필 큐가 아니라 이 패스입니다.

모든 외부 요청은 먼저 토큰 버킷에서 토큰을 하나 가져옵니다. 버킷은 초당 BS_API_RPS 개(기본 8)로 채워지고 용량은 BS_API_BURST(기본 16)입니다. 버킷이 단일 공유 객체이므로 사이클이나 수동 갱신이 몇 개가 겹쳐도 속도는 지켜집니다. 버킷이 비어 있으면 호출자는 스핀하지 않고 필요한 시간만큼 정확히 기다립니다.

사이클은 두 방식으로 구동됩니다. 하나는 프로세스 내 인터벌로, SYNC_INTERVAL_MS(기본 30초)마다 최대 SYNC_BUDGET 개 요청(기본 100)을 씁니다. 평상시에는 대략 절반이 전투 기록 수확, 약 3분의 1이 플레이어 프로필, 나머지가 클럽이며, 첫 프로필 수집을 기다리는 새로 발견된 계정이 1,000개를 넘는 동안에는 프로필 우선으로 뒤집힙니다. 다른 하나는 백그라운드 인터벌을 유지할 수 없는 플랫폼에서 외부 스케줄러가 /api/cron 을 호출하는 방식입니다. 겹친 사이클은 요청 속도를 배가시키지 않고 즉시 반환하고, 부팅 후 첫 실행은 몇 초 무작위로 지연되어 재시작 폭풍이 API를 한꺼번에 때리지 않으며, 429나 IP 불일치 403은 닫힌 문을 계속 두드리는 대신 사이클을 조기에 중단합니다. 6시간마다 정리 패스가 90일 넘은 보관 전투, 120일 넘은 집계, 1년 넘은 스냅숏을 삭제합니다.

플레이어·클럽 페이지의 「지금 갱신」 버튼은 태그당 20초 쿨다운이 있는 별도 엔드포인트를 통해 모든 TTL을 우회하므로, 버튼을 계속 눌러도 증폭기가 되지 않습니다. /api/health 는 실시간/데모 모드, 색인 크기, 보관량, 큐 깊이, 마지막 오류를 보고합니다.

전투 보관소

API는 플레이어의 최근 25경기만 반환하며 히스토리 엔드포인트가 없습니다. 이 사이트에서 25경기보다 과거를 볼 수 있는 모든 기능은 그 창이 보관되기에 존재합니다. 프로필 조회와 큐 갱신마다 새로운 것이 저장되며, 키를 붙여 같은 전투를 두 번 봐도 no-op이 되게 합니다.

각 전투는 보관되는 동시에 일·모드·맵·구간·브롤러를 키로 하는 일별 집계 테이블에도 반영되어 픽·승리·무승부·스타 플레이어 횟수를 셉니다. 전투 하나당 네 행이 갱신됩니다 — 실제 맵과 합성 all 맵을 각각 두 번씩: 한 번은 거친 구간(ranked 또는 trophies), 한 번은 정확한 밴드(ranked:masters, trophies:500-750) 아래에. all 맵 행 덕분에 모드 전체 티어 리스트가 스캔이 아닌 단일 색인 읽기가 되고, 밴드 행 덕분에 티어 리스트의 랭크·트로피 범위 필터가 장식이 아니라 실제가 됩니다. 친선전과 플레이어의 브롤러를 특정할 수 없는 전투는 건너뜁니다.

쇼다운에는 승패 필드가 없고 순위만 있으므로 상위 절반은 승리로 계산합니다: 솔로는 10명 중 5위 이내, 듀오는 5팀 중 3위 이내, 그 외는 3팀 중 2위 이내. 이 규칙은 승률·티어 리스트·브롤러별 통계에 일관되게 적용됩니다.

보관소는 이 인스턴스가 본 계정에서 만들어지므로 전체 경기 모집단이 아닌 표본이며, 조회되는 계정 쪽으로 치우친 표본입니다. 이는 상대적 형태보다 절대 수치에 훨씬 크게 영향을 줍니다.

티어 리스트 채점 방식

순수 승률로 줄 세우면 지난주에 40판으로 반짝한 비주류 브롤러가 꼭대기에 옵니다. 픽률로 줄 세우는 것은 인기의 다른 이름일 뿐입니다. 점수는 둘을 섞되, 승률은 실제로 믿을 수 있는 만큼만 반영합니다.

rows          = { b : picks(b) ≥ 200 }
globalWinRate = Σ wins / Σ picks

shrunk        = (wins + globalWinRate * 1500) / (picks + 1500)
pickRate      = picks / total picks
popularity    = log10(1 + pickRate * rows * 3) / log10(4)

score         = (shrunk - globalWinRate) * 100 + popularity * 1.8

1500은 가상 전투 수로 표현한 사전 분포입니다. 200픽인 브롤러는 전체 평균 쪽으로 크게 끌려가고, 30,000픽인 브롤러는 사실상 자기 승률을 유지합니다. 경험적 베이즈 수축 아이디어를 투박하지만 투명하게 적용한 것입니다. 인기 항은 로그이므로, 10배 더 픽되는 브롤러는 10배 점수가 아니라 약간의 보너스를 받습니다.

그다음 티어는 고정 문턱이 아니라 점수 분포에서 잘라냅니다. 입선한 브롤러들에서 평균과 표준편차를 계산하고, 각 브롤러를 평균에서 몇 표준편차 위·아래인지로 배치합니다 — S 는 +1.25, A 는 +0.5, B 는 −0.25, C 는 −1, 그 아래는 D. 알아둘 만한 귀결: 티어는 현재 메타에 상대적이므로 메타가 평평해도 리스트에는 항상 S 티어가 있습니다.

티어 리스트는 선택한 창에 픽이 5,000개 이상 쌓였을 때만 실제 데이터로 만들어집니다(기본 창은 7일). 그 미만이면 페이지는 결정적 생성기로 물러나며, 몇백 판으로 자신만만한 순위를 보여주는 대신 스스로 데모 데이터라고 표시합니다.

API에서 그대로

다음은 공식 응답에서 읽어 그대로 표시하는 값입니다. API는 대부분의 커뮤니티 래퍼가 모델링하는 것보다 많이 공개하므로, 다른 사이트가 추정하는 여러 숫자가 여기서는 실측값입니다.

  • 트로피, 최고 트로피, 경험 레벨, 3v3/솔로/듀오 승리 수
  • 경쟁전 티어 인덱스와 표시 이름, 경쟁전 elo, 시즌 id
  • 시즌·역대 최고 경쟁전 티어와 elo
  • 총 프레스티지 레벨, 명성과 명성 등급
  • 브롤러별: 파워, 랭크, 트로피, 프레스티지, 현재·최장 연승
  • 브롤러별: 가젯, 스타 파워, 기어, 하이퍼차지, 장착 스킨
  • 클럽 소속, 클럽 명단과 멤버 역할, 필요 트로피
  • 국가별 랭킹과 실시간 이벤트 로테이션
  • 플레이어별 최근 25경기(모드, 맵, 유형, 결과, 트로피 변동 포함)
  • 브롤러 로스터 자체. 동기화 스크립트가 생성 파일로 가져옵니다 — 마지막 동기화 2026. 09. 15.

본 사이트에서 도출

다음은 어떤 형태로도 API에 존재하지 않습니다. 이 인스턴스가 관측한 것에서 계산되며, 표시되는 모든 곳에 그 사실이 표기됩니다.

  • 이름 기록과 클럽 기록 — 연속 폴링의 차이를 비교해 구축
  • 트로피·elo·프레스티지·명성 추이 — 매시간 스냅숏 테이블에서
  • 티어 리스트, 승률, 픽률, 스타 플레이어 비율 — 전투 보관소에서
  • 스킨 사용률 랭킹 — 색인된 프로필의 장착 스킨 필드를 집계
  • 경쟁전 티어 라벨 표 — 하드코딩이 아니라 관측된 프로필에서 학습
  • 추정 플레이 시간 — 승리 수와 경험 레벨에서 약 ±15%. API가 시계를 공개하지 않기 때문
  • 경쟁전 점수 타임라인 — 경쟁전 전투에는 점수 변동이 없어 승패를 약 ±30으로 모델링해 시즌의 형태를 재구성
  • 브롤러별 희귀도와 클래스 — API가 빠뜨린 유일한 로스터 필드로, 내장 표에서 이름으로 찾고 추측하지 않고 unknown 으로 남김
  • 계산기 표(강화 비용, 트로피 변동, 스타 드롭 확률, 트로피 로드) — 게임에서 옮겨 적음, 스냅숏 2026-09-16

아직 모델인 부분

일부 페이지는 아직 실제 데이터로 뒷받침되지 않으며, 추측을 조용히 보여주는 대신 페이지 스스로 그렇다고 밝힙니다:

  • 경쟁전 분포. 이 페이지는 완전히 색인된 각 프로필에 기록된 경쟁전 티어에 대한 실제 쿼리에 연결되어 있으며, 티어를 가진 색인 프로필이 200개 미만인 동안에만 모델링된 래더로 물러납니다 — 그 미만에서는 모집단이 작고 치우쳐 형태가 오해를 부르기 때문입니다. 물러날 때는 그 사실과 현재 개수를 명시합니다.
  • 표본이 얇은 티어 리스트. 보관된 픽이 5,000개 미만인 모드·맵·구간은 몇 판으로 순위를 매기는 대신 모델 분포로 물러납니다. 페이지는 현재 개수와 필요한 개수를 표시합니다.
  • 핀, 배틀 카드, 프로필 칭호. 이들은 어떤 형태로도 API에 없으므로, 해당 랭킹 페이지는 명시적 안내문이 붙은 레이아웃 시연입니다.
  • 데모 모드. API 토큰이 없으면 사이트 전체가 검색한 태그를 시드로 하는 결정적 생성기로 동작해, 같은 태그는 항상 같은 숫자를 냅니다. 영향을 받는 모든 페이지에 경고 배너가 붙습니다.

색인이 검색 결과에 무엇을 뜻하는지는 검색 사양 를 참고하세요.

기술 정보 · BrawlPeek