// 이 파일이 하는 일: "시스템 설계 훈련 2 — 실시간 채팅 시스템" 코스 — URL 단축기(훈련 1)와
// 결정적으로 다른 '상태 있는 장수명 연결'의 세계를 8개 섹션으로 안내하는 정적 학습 페이지.
// 요구사항 자르기 → 연결 기술(WebSocket) → 서버 상태와 Pub/Sub fan-out → 저장·순서 →
// 전달 보장·오프라인 동기화 → 읽음·presence → 그룹 fan-out 전략 → 전체 조립·장애 방어까지.
// 프레이밍은 "우리가 이미 가진 것과 잇기" — traderbox의 SSE 시세 스트림·MarketBus(Valkey
// Pub/Sub)·라이브 채팅 레이트리밋 경험을 다리 삼아, 카카오톡급 채팅의 설계 감각을 기른다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

import { Link } from 'react-router-dom';

// ── 예제 코드 상수들 ──

const CODE_SCOPE = `[면접식 요구사항 자르기 — "채팅 만들어 보세요"에 바로 그리지 말 것]

기능 요구 (오늘 범위 O)              오늘 범위 밖 (X — 말하고 자른다)
─────────────────────────────────────────────────────────────
1:1 채팅 + 소규모 그룹(최대 300명)    통화·화상 (별도 미디어 서버의 세계)
텍스트 메시지 + 전달/읽음 표시        E2E 암호화 (키 관리가 별도 주제)
온라인 상태(presence) 표시           10만 명 방송 채널 (섹션 7에서 경계만)
오프라인이었다가 접속 시 밀린 메시지   메시지 검색 (검색 인덱스는 훈련 3 이후)

비기능 요구 — 숫자로 박아 두기
─────────────────────────────────────────────────────────────
전달 지연: 같은 방 상대에게 수백 ms 이내 (체감 '즉시')
유실:     서버가 ACK한 메시지는 절대 유실 불가
규모:     동시 접속 수백만 연결, 일 메시지 수십억 건
순서:     같은 채널 안에서는 모두가 같은 순서로 본다`;

const CODE_TRANSPORT = `[클라이언트 ↔ 서버 실시간 통신 3형제]

① 숏/롱 폴링 — 클라가 반복해서 "새 거 있어요?"
   숏폴링: 1초마다 HTTP 요청. 수백만 명이면 빈손 요청이 서버를 때린다.
   롱폴링: 서버가 새 메시지 올 때까지 응답을 붙잡음. 낫지만
           메시지마다 연결을 다시 맺는 비용 + 양방향은 여전히 어색.

② SSE (Server-Sent Events) — HTTP 위의 단방향 방송
   서버 → 클라 한 방향 스트림. 자동 재접속 내장, 프록시 친화적.
   traderbox 시세가 정확히 이 케이스: 서버가 틱을 '뿌리기만' 하면 됨.
   클라 → 서버는 별도 HTTP POST로 — 방송엔 충분, 대화엔 반쪽.

③ WebSocket — 하나의 TCP 연결 위 양방향 프레임
   최초 HTTP 핸드셰이크(Upgrade) 후 프로토콜 전환.
   클라·서버 어느 쪽이든 즉시 밀어넣기(push) 가능.
   채팅 = "서로가 서로에게 수시로 말 걸기" → WebSocket이 정답.

결론: 단방향 방송이면 SSE로 충분하고(시세·알림), 양방향 대화면 WebSocket.
기술은 요구사항이 고른다 — 유행이 고르지 않는다.`;

const CODE_REGISTRY = `// 문제: 유저 A는 chat-server-1에, 유저 B는 chat-server-7에 붙어 있다.
// A가 보낸 메시지를 B에게 주려면, 누군가 "B가 어디 있는지" 알아야 한다.

// 방법 1 — 세션 레지스트리: 접속할 때 위치를 공유 저장소에 기록
// (Valkey 해시: 유저 → 서버 매핑, TTL로 죽은 항목 자동 청소)
HSET session:user:B  server chat-server-7  connectedAt 1721455200
EXPIRE session:user:B 60          // 하트비트마다 갱신 — 안 오면 소멸

// 방법 2 — Pub/Sub 백본: 위치를 아예 안 찾고 '방송'으로 해결
// 각 채팅 서버는 자기에게 붙은 유저들의 채널을 구독해 둔다.
SUBSCRIBE channel:room:42          // chat-server-1도, 7도 구독 중
PUBLISH   channel:room:42  '메시지 페이로드'
// → 구독 중인 모든 서버가 받아서, 자기에게 붙은 참여자에게만 소켓으로 전달

// traderbox MarketBus가 정확히 이 구조다:
// 수집기가 Valkey 채널 market.ticks에 PUBLISH → 각 서버의
// MarketStreamService가 구독 → 자기에게 붙은 SSE 클라이언트에 fan-out.
// '시세 틱'을 '채팅 메시지'로 바꾸면 그대로 채팅 백본이 된다.`;

const CODE_MSGID = `[메시지 ID 설계 — 순서와 페이지네이션을 한 번에]

요구: 같은 채널 안에서 모두가 같은 순서로 본다 + "이 메시지 이후 것 주세요" 조회

나쁜 답: 클라이언트가 찍은 시각 → 기기마다 시계가 다르다(클럭 스큐)
아쉬운 답: 전역 auto-increment 1개 → 전 세계 메시지가 한 카운터에 줄 섬 (병목)

좋은 답: '채널 안에서만' 단조 증가하는 ID
  msgId = (채널별 시퀀스)     // 채널 42의 1, 2, 3... — 채널끼리는 독립
  또는 Snowflake 변형: [타임스탬프 41bit][서버 10bit][시퀀스 12bit]
  → 시간순 정렬 가능 + 발급이 분산돼 병목 없음

이 ID 하나로 세 문제가 풀린다:
  · 순서:        채널 내 ID 오름차순 = 모두가 보는 순서
  · 페이지네이션: WHERE channel=42 AND id < 커서 ORDER BY id DESC LIMIT 50
  · 동기화 커서:  "id 1037까지 받았어요" → 그 뒤만 다시 받기 (섹션 5)

전역 순서(모든 채널을 통틀어 한 줄)는 아무도 요구하지 않았다.
보장 범위를 채널로 좁히는 순간, 분산 시스템의 난제가 평범한 문제가 된다.`;

const CODE_ACK = `[전달 보장 — 송신 확인(ACK) 3단계]

A(보낸 사람)                 서버                      B(받는 사람)
────────────────────────────────────────────────────────────────
send(msg, tempId) ────────▶
                    ① 수신 + 저장소에 기록 (여기부터 유실 불가 구간)
◀──────── ack:saved(tempId → msgId)     // 클라: 시계 1개 ✓
                    ② B의 연결 찾아 push ────────▶ 수신
                                        ◀──────── ack:delivered(msgId)
◀──────── ack:delivered                 // 시계 2개 ✓✓
                    ③ B가 읽음 커서 갱신 (섹션 6)
◀──────── read-cursor 갱신 이벤트        // 읽음 ✓✓ (파란색)

핵심 규칙
· ①이 오기 전엔 클라가 tempId로 재전송해도 된다 — 서버가 걸러 줄 테니.
· 오프라인 B: ②가 불가능 → 저장소에 남겨 두고, 푸시 알림으로 깨운다.
· B가 다시 접속하면: "내 커서는 1037" → 서버가 1038부터 몰아서 전달 (inbox 동기화)
· 재전송·재동기화가 겹치면 같은 메시지가 두 번 올 수 있다
  → 클라이언트는 msgId 기준으로 dedupe (멱등 수신). 화면에 두 번 그리지 않는다.

전달 보장의 진짜 모양 = "정확히 한 번 전송"이 아니라
"최소 한 번 전송 + 수신 측 멱등"이다. 네트워크 위에서 전자는 존재하지 않는다.`;

const CODE_READ = `[읽음 표시 — 플래그가 아니라 커서]

나쁜 모델: 메시지 × 참여자마다 read 플래그
  300명 방에 메시지 1개 → 읽음 행 300개, 메시지 1만 개 → 행 300만 개
  쓰기 폭발 + "안 읽은 수" 세려면 매번 집계

좋은 모델: 참여자마다 '어디까지 읽음' 포인터 1개
  read_cursor(user=B, channel=42) = 1037
  · 읽음 처리: 포인터 1개 UPDATE (몇 개를 읽었든 쓰기 1번)
  · 안 읽은 수: 채널 최신 ID − 내 커서 = 즉시 계산
  · "누가 읽었나": 커서 ≥ 그 메시지 ID인 참여자 목록

[presence(온라인 표시) — 정확함보다 싸게]
  접속 중 클라가 30초마다 하트비트 → Valkey에 TTL 60초로 기록
  SET presence:user:B online EX 60
  · 키가 있으면 온라인, 없으면 오프라인 — 끊겨도 최대 60초 안에 자연 소멸
  · 몇 초 늦게 '오프라인'로 바뀌어도 아무도 다치지 않는다 → 최종 일관성이면 충분
  · 잔인한 산수: 300명 방에서 전원의 읽음·presence 변화를 전원에게 실시간 중계하면
    이벤트 1건 → 300 fan-out, 300명이 움직이면 300×300 = 9만 건/틱.
    그래서 실무는 묶어 보내기(batch)·간격 제한(throttle)·방 화면 볼 때만 구독으로 깎는다.`;

const CODE_FANOUT = `[그룹 크기에 따라 전략이 뒤집힌다]

쓰기 fan-out (push): 보낼 때 참여자 전원의 inbox에 미리 복사
  · 읽기가 빛의 속도 (내 inbox만 보면 끝)
  · 1:1, 소그룹(수십~수백)에 적합 — 카톡 방 대부분이 여기
  · 10만 명 채널이면? 메시지 1개에 쓰기 10만 번 — 자폭

읽기 fan-out (pull): 채널에 1번만 쓰고, 읽는 사람이 그때 가져감
  · 쓰기가 가볍다 (1회) · 읽을 때 채널을 조회하는 비용을 냄
  · 대형 방송 채널에 적합 — 구독자가 많을수록 유리

하이브리드(실무 정답): 임계값(예: 참여자 1천 명)으로 갈라 탄다
  작은 방 → push / 거대 채널 → pull / 유명인 방 → 팔로워는 pull, 친한 친구는 push

10만 명 '채널'의 정체: 모두가 말하는 채팅이 아니라
소수가 쓰고 다수가 읽는 구조 = 사실상 피드(feed)다.
→ 다음 강좌(뉴스피드와 타임라인)가 정확히 이 지점에서 시작한다.`;

const CODE_ARCH = `[완성된 컴포넌트 다이어그램 — 말로 그릴 수 있어야 한다]

클라이언트(웹·앱)
   │  wss:// (WebSocket, 스티키 없이도 OK — 상태는 레지스트리에)
   ▼
L4/L7 로드밸런서 ──▶ 채팅 서버군 (stateless에 가깝게: 연결만 들고, 진실은 밖에)
                        │            │
          ┌─────────────┤            └──────────────┐
          ▼             ▼                           ▼
   Valkey (Pub/Sub    메시지 저장소                 푸시 게이트웨이
    백본 + 세션        (쓰기 多·최근 조회 편중       (오프라인 유저 →
    레지스트리 +        → 채널+시간 파티셔닝,        FCM/APNs로 깨우기)
    presence TTL)      와이드컬럼 or 파티션드 RDB)

장애 시나리오 3종 세트 — 설계 리뷰에서 반드시 찔러 본다
① Pub/Sub 다운  → 실시간 전달만 멈춘다. 저장은 계속되므로 유실은 없다.
                  복구 후 클라 커서 재동기화로 따라잡기. (저장이 진실, 방송은 가속)
② 저장소 지연   → ack:saved가 늦어진다. 클라는 '보내는 중...' 표시 + 재시도.
                  절대 하지 말 것: 저장 전에 saved ACK 보내기 (유실 불가 약속 파기)
③ 채팅 서버 재시작 → 수만 연결이 동시에 재접속 (thundering herd)
                  → 클라에 지수 백오프 + 지터(무작위 지연)를 심어 파도를 넓게 편다`;

const CODE_PRACTICE = `# 실습(85분 중 후반 40분): 서버 2대 × Valkey Pub/Sub 미니 채팅
# 목표: '서로 다른 서버에 붙은 두 브라우저'가 실시간 대화 — 오늘 배운 백본을 손으로.

# ① Valkey 하나 띄우기 (채팅 백본)
docker run -d --name chat-valkey -p 6379:6379 valkey/valkey:8

# ② 같은 서버 코드를 포트만 바꿔 2개 인스턴스로
#    server.js 요지 (ws + ioredis):
#    - 브라우저가 ws://localhost:400X 로 접속
#    - 소켓으로 온 메시지 → PUBLISH chat.room1
#    - SUBSCRIBE chat.room1 로 받은 메시지 → 내게 붙은 모든 소켓에 send
#    (발행자 자신도 구독으로 돌려받는다 — echo 중복은 msgId로 dedupe: 섹션 5 복습)
PORT=4001 node server.js     # 채팅 서버 A
PORT=4002 node server.js     # 채팅 서버 B

# ③ 브라우저 두 창: 하나는 4001, 하나는 4002 접속 페이지 열기
#    → 다른 서버에 붙었는데 대화가 된다 = Pub/Sub fan-out을 눈으로 확인

# ④ 방어 연습: docker stop chat-valkey 후 메시지를 보내 보라.
#    무엇이 멈추고, 무엇이 살아 있는가? 복구(docker start) 후엔?
#    이 관찰을 섹션 8의 장애 시나리오 ①과 대조해 보며 마무리.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
function Section({ n, title, sub, children }) {
  return (
    <section className="step-card" id={`sec-${n}`}>
      <div className="step-head">
        <span className="step-num">{n}</span>
        <div>
          <h3>{title}</h3>
          {sub && <span className="step-sub">{sub}</span>}
        </div>
      </div>
      <div className="step-body">{children}</div>
    </section>
  );
}

function Code({ children }) {
  return <pre className="code-block"><code>{children}</code></pre>;
}

const SECTIONS = [
  { n: 1, label: '요구사항 자르기' },
  { n: 2, label: '연결 기술 선택' },
  { n: 3, label: '서버 상태와 fan-out' },
  { n: 4, label: '저장과 순서 보장' },
  { n: 5, label: '전달 보장·오프라인' },
  { n: 6, label: '읽음·presence' },
  { n: 7, label: '그룹과 대형 채널' },
  { n: 8, label: '조립과 방어 연습' },
];

export default function Lv4SystemDesignChatPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 특급 과정</div>
        <h1>시스템 설계 훈련 2<br />— 실시간 채팅 시스템</h1>
        <p>
          훈련 1의 URL 단축기는 요청이 오면 답하고 <strong>끊어지는</strong> 시스템이었어요.
          채팅은 다릅니다. 수백만 개의 연결이 <strong>몇 시간씩 살아 있고</strong>, 서버가
          먼저 말을 걸고, 유실은 용납되지 않죠. 이번 훈련에서는 WebSocket 연결 관리부터
          메시지 순서, 읽음 표시, 오프라인 동기화까지 — <strong>상태 있는 장수명 연결</strong>의
          세계를 설계합니다. 우리가 운영해 본 SSE 시세 스트림과 MarketBus가 좋은 디딤돌이
          되어 줄 거예요.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 85분</span>
          <span className="chip">실습: 서버 2대 × Valkey Pub/Sub 채팅</span>
          <span className="chip">선수 지식: 훈련 1(URL 단축기)·HTTP/WebSocket 기초</span>
        </div>
      </div>

      {/* 섹션 바로가기 */}
      <nav className="pill-nav">
        {SECTIONS.map((s) => (
          <a key={s.n} href={`#sec-${s.n}`}>
            <b>{s.n}</b>
            {s.label}
          </a>
        ))}
      </nav>

      <Section n={1} title="요구사항 정리 — 채팅은 생각보다 넓다" sub="범위를 먼저 자르는 사람이 설계를 지배한다">
        <p>
          "채팅 시스템을 설계해 보세요"라는 문장 안에는 사실 <strong>여러 개의 제품</strong>이
          숨어 있어요. 1:1 대화, 300명 그룹, 10만 명 방송 채널, 온라인 표시, 읽음 확인,
          사진·영상 전송, 통화까지 — 전부 "채팅"이라고 불리죠. 훈련 1에서 배운 첫 동작을
          그대로 씁니다. <strong>그리기 전에 자른다.</strong> 무엇을 만들고 무엇을 오늘 범위
          밖으로 밀지 소리 내어 확정하는 것이 설계의 첫 문장이에요.
        </p>
        <Code>{CODE_SCOPE}</Code>
        <p>
          비기능 요구 중 두 개가 이 시스템의 성격을 결정합니다. 첫째,{' '}
          <strong>전달 지연 수백 ms</strong> — 상대가 보낸 말이 '즉시' 떠야 하니, 클라이언트가
          주기적으로 물어보는 방식으론 부족하고 서버가 <strong>밀어넣는(push)</strong> 경로가
          필요해요. 둘째, <strong>유실 불가</strong> — 은행 이체만큼은 아니어도, "보냈는데
          상대에게 안 간 메시지"는 채팅 서비스의 신뢰를 끝장냅니다. 빠르되 잃지 않기, 이
          긴장이 코스 전체를 관통합니다.
        </p>
        <div className="tip">
          <b>훈련 1과의 결정적 차이</b> URL 단축기는 요청↔응답이 끝나면 서버가 그 사용자를{' '}
          <strong>잊어도</strong> 됐어요(무상태). 채팅 서버는 "지금 누가 나에게 접속해
          있는가"라는 <strong>상태</strong>를 연결이 살아 있는 내내 들고 있어야 합니다.
          상태 있는 장수명 연결 — 이 한 구절이 이번 훈련에서 만나는 거의 모든 어려움의
          출처예요. 스케일아웃도, 배포도, 장애 복구도 전부 이것 때문에 어려워집니다.
        </div>
      </Section>

      <Section n={2} title="연결 기술 선택 — 폴링, SSE, WebSocket" sub="단방향 방송이면 SSE, 양방향 대화면 WebSocket">
        <p>
          "서버가 먼저 말을 건다"를 이루는 기술은 크게 셋이에요. 반복해서 물어보는{' '}
          <strong>폴링</strong>, 서버→클라이언트 한 방향 스트림인 <strong>SSE</strong>,
          그리고 하나의 연결 위에서 양쪽이 자유롭게 밀어넣는 <strong>WebSocket</strong>.
          각각이 나쁜 기술이어서가 아니라, <strong>요구사항이 다른 답을 고르게</strong> 합니다.
        </p>
        <Code>{CODE_TRANSPORT}</Code>
        <p>
          우리에게는 살아 있는 비교 대상이 있죠. traderbox의 시세 스트림은{' '}
          <span className="icode">MarketStreamService</span>가 SSE로{' '}
          <span className="icode">event:tick</span>을 흘려보내는 구조예요. 시세는 서버가
          클라이언트에게 <strong>일방적으로 방송</strong>하면 끝이라 SSE가 정확히 맞습니다.
          반면 채팅은 클라이언트도 수시로 말을 얹어야 하는 <strong>양방향 대화</strong>라서,
          요청마다 새 HTTP를 여는 반쪽짜리 조합 대신 WebSocket이 자연스러운 답이 돼요.
        </p>
        <p>
          다만 장수명 연결은 <strong>인프라의 협조</strong>를 요구합니다. 우리가 SSE를
          운영하며 이미 데인 적 있는 함정들 그대로예요. 중간의 프록시·게이트웨이가{' '}
          <strong>유휴 연결을 60초 만에 끊어 버리거나</strong>, 응답을 gzip <strong>버퍼링</strong>
          하느라 실시간성이 죽거나(우리 프론트가 <span className="icode">compress:false</span>를
          고집하는 이유), 로드밸런서가 연결 수가 아닌 요청 수로 분배해 <strong>특정 서버에
          연결이 편중</strong>되거나. WebSocket도 같은 벽을 전부 만납니다 — 기술을 고르는 일은
          그 기술이 지나갈 <strong>길목 전체</strong>를 점검하는 일이에요.
        </p>
        <div className="tip">
          <b>면접 화법</b> "무조건 WebSocket이요"보다 강한 답: "알림처럼 단방향이면 SSE로
          충분하고 운영도 단순합니다. 채팅은 양방향 저지연이 요구라 WebSocket을 쓰되, 프록시
          타임아웃·LB 연결 편중을 함께 설계하겠습니다." — <strong>탈락시킨 선택지의 이유</strong>를
          말하는 사람이 신뢰를 얻습니다.
        </div>
      </Section>

      <Section n={3} title="채팅 서버의 상태 문제" sub="유저는 어느 서버에 있는가 — Pub/Sub 백본으로 풀기">
        <p>
          서버를 수평 확장하는 순간 채팅만의 문제가 태어납니다. 유저 A는{' '}
          <span className="icode">chat-server-1</span>에, 유저 B는{' '}
          <span className="icode">chat-server-7</span>에 WebSocket이 붙어 있어요. A의 메시지를
          B에게 주려면 <strong>서버끼리</strong> 전달해야 하죠. URL 단축기에는 없던, 연결이
          상태이기 때문에 생기는 문제예요.
        </p>
        <Code>{CODE_REGISTRY}</Code>
        <p>
          실무에서는 두 방법을 섞습니다. <strong>Pub/Sub 백본</strong>으로 채널 메시지를
          서버들에 방송해 fan-out을 해결하고, <strong>세션 레지스트리</strong>는 특정 유저를
          정조준해야 할 때(1:1 알림, 강제 로그아웃 등) 씁니다. 백본은 Valkey Pub/Sub처럼
          가볍고 빠른 것으로 시작하고, "구독자가 없던 순간의 메시지도 남아야 한다"면 Kafka처럼
          로그가 남는 것으로 올라가요 — 우리 스택이 감사 이벤트를 Kafka로, 실시간 틱을 Valkey로
          나눠 태우는 것과 같은 감각입니다.
        </p>
        <p>
          그리고 상태 있는 서버의 숙명, <strong>재접속 폭풍</strong>. 연결 5만 개를 든 서버를
          재시작(배포!)하면 5만 클라이언트가 <strong>같은 순간</strong> 재접속을 시도해 남은
          서버들을 때립니다. 방어는 클라이언트에 심어요 — 재시도 간격을 지수적으로 늘리는{' '}
          <strong>지수 백오프</strong>에, 전원이 같은 박자로 몰리지 않게 무작위 지연{' '}
          <strong>지터(jitter)</strong>를 더하는 것. 서버 쪽에서는 연결을 한 번에 다 끊지 않고
          천천히 흘려보내는(drain) 배포 절차가 짝을 이룹니다.
        </p>
        <div className="tip">
          <b>이미 본 구조다</b> <span className="icode">MarketBus</span>가 Valkey 채널{' '}
          <span className="icode">market.ticks</span>에 PUBLISH하면 각 인스턴스가 구독해서
          자기 SSE 클라이언트에게 뿌리죠. 페이로드를 시세 틱에서 채팅 메시지로 바꾸면 그게
          바로 채팅 백본입니다. 오늘 실습(섹션 8)에서 이 구조를 최소 형태로 직접 만들어 봐요.
        </div>
      </Section>

      <Section n={4} title="메시지 저장과 순서 보장" sub="쓰기 많고 최근만 읽는다 — 그리고 순서는 채널 단위면 충분">
        <p>
          채팅 이력의 접근 패턴은 뚜렷해요. <strong>쓰기가 압도적으로 많고</strong>(일 수십억
          건), 읽기는 <strong>최근 메시지에 극단적으로 편중</strong>됩니다(방을 열면 마지막
          50개, 위로 스크롤하면 그 이전 50개). 조인도 복잡한 트랜잭션도 없죠. 그래서 채팅
          저장소는 <strong>채널+시간 기준 파티셔닝</strong>이 자연스럽고, 쓰기에 강한
          와이드컬럼 계열(HBase·Cassandra류)이 대규모에서 자주 선택돼요. 물론 우리 규모라면
          파티셔닝한 Postgres로도 충분히 출발할 수 있습니다 — 저장소의 이름보다{' '}
          <strong>접근 패턴을 먼저 말하는 것</strong>이 훈련의 핵심이에요.
        </p>
        <Code>{CODE_MSGID}</Code>
        <p>
          여기서 훈련 1에서 익힌 무기를 다시 씁니다. <strong>보장 범위를 좁혀서 난제를
          평범하게 만들기.</strong> "모든 메시지의 전역 순서"는 분산 시스템의 악몽이지만,
          곰곰이 보면 아무도 그걸 원하지 않아요. 필요한 건 <strong>같은 방을 보는 사람들이
          같은 순서를 보는 것</strong>뿐 — 즉 채널 단위 순서입니다. 채널 안에서만 단조 증가하는
          ID를 쓰면 순서·페이지네이션·동기화 커서(다음 섹션)까지 한 번에 풀리고, 채널끼리는
          완전히 독립이라 병목도 없습니다.
        </p>
        <div className="tip">
          <b>클라이언트 시계는 믿지 않는다</b> 메시지 순서를 "보낸 기기의 시각"으로 정하면,
          시계가 3분 느린 폰의 메시지가 과거로 꽂힙니다. 순서의 기준은 언제나{' '}
          <strong>서버가 발급한 ID</strong>예요. 클라이언트 타임스탬프는 "오후 3:42" 같은{' '}
          <strong>표시용</strong>으로만 씁니다. 결정하는 값과 보여 주는 값을 분리하는 것 —
          분산 시스템의 오랜 위생 습관입니다.
        </div>
      </Section>

      <Section n={5} title="전달 보장과 오프라인 동기화" sub="ACK 3단계, 커서 동기화, 그리고 수신 측 멱등">
        <p>
          "보낸 메시지는 유실 불가"라는 약속을 기술로 번역하면 <strong>단계별 확인(ACK)</strong>이
          됩니다. 카카오톡에서 본 그 표시들이 사실 프로토콜이에요 — 서버가 받아 저장했는지,
          상대 기기까지 갔는지, 상대가 읽었는지를 각각 다른 신호로 확인합니다.
        </p>
        <Code>{CODE_ACK}</Code>
        <p>
          <strong>오프라인 유저</strong>가 이 그림의 시험대예요. B가 지하철에서 끊겨 있는 동안
          온 메시지들은 저장소에 안전히 남아 있고(①에서 이미 저장됐으니), 푸시 알림이 B를
          깨웁니다. B가 재접속하면 협상은 한 문장이에요 — <strong>"내 커서는 1037이야."</strong>{' '}
          서버는 채널별로 1038부터를 몰아서 내려 주죠. 섹션 4의 단조 증가 ID가 여기서
          동기화 커서로 <strong>재활용</strong>되는 겁니다. 유저별 밀린 메시지함을 커서로
          따라잡는 이 방식을 <strong>inbox 모델</strong>이라고 불러요.
        </p>
        <p>
          마지막 조각은 <strong>중복</strong>입니다. "ACK가 안 와서 재전송했는데 사실 첫 번째가
          도착해 있었다", "실시간 push와 커서 동기화가 겹쳤다" — 분산 환경에서 중복 수신은
          예외가 아니라 <strong>일상</strong>이에요. 그래서 수신 클라이언트는{' '}
          <span className="icode">msgId</span>를 기준으로 이미 가진 메시지를 버립니다(dedupe).
          같은 메시지가 두 번 와도 화면엔 한 번만 — 이것이 <strong>멱등 수신</strong>이고,
          "최소 한 번 전송 + 수신 멱등"의 조합이 네트워크 위에서 구현 가능한 전달 보장의
          진짜 모양입니다.
        </p>
        <div className="tip">
          <b>훈련 1의 메아리</b> URL 단축기에서 "생성 요청 재시도 시 같은 단축코드를 돌려주기"로
          멱등을 배웠다면, 채팅에서는 <strong>수신 쪽</strong> 멱등을 배웁니다. 위치는 달라도
          원리는 하나예요 — <strong>같은 일이 두 번 일어나도 결과는 한 번과 같게.</strong>{' '}
          분산 시스템 설계의 답안지에 가장 자주 등장하는 단어이니 몸에 붙여 두세요.
        </div>
      </Section>

      <Section n={6} title="읽음 표시·온라인 상태 — 비싼 부가 기능" sub="플래그 대신 커서, 정확함 대신 TTL">
        <p>
          "읽음 1이 사라지는 것"과 "초록 불"은 사용자에겐 당연한 기능이지만, 설계자에겐{' '}
          <strong>본문 메시지보다 트래픽이 많아질 수 있는</strong> 골칫거리예요. 사람은 메시지를
          보내는 것보다 읽고·접속하고·나가는 일을 훨씬 자주 하니까요. 그래서 이 둘은 "얼마나
          정확해야 하는가"를 먼저 협상하고 들어갑니다.
        </p>
        <Code>{CODE_READ}</Code>
        <p>
          <strong>읽음</strong>은 모델 선택이 승부예요. 메시지마다 읽음 플래그를 남기는 대신,
          참여자마다 <strong>"어디까지 읽었다" 포인터 하나</strong>만 유지합니다. 섹션 4의
          단조 증가 ID 덕에 "커서 이하는 전부 읽음"이 성립하니, 몇백 개를 한 번에 읽어도
          쓰기는 UPDATE 한 번이죠. 같은 ID 하나가 순서 → 페이지네이션 → 동기화 → 읽음까지
          네 번째 일을 하고 있다는 것, 눈치채셨나요? <strong>좋은 원시(primitive) 하나가
          기능 여럿을 공짜로 만듭니다.</strong>
        </p>
        <p>
          <strong>presence</strong>는 정확성을 흥정하는 훈련이에요. 초록 불이 몇 초 늦게
          꺼진다고 메시지가 유실되나요? 아니죠. 그래서 하트비트 + TTL이라는 값싼 구조로
          충분하고, 이것이 <strong>최종 일관성(eventual consistency)으로 충분한 데이터</strong>의
          교과서적 예입니다. 반대로 메시지 본문은 유실 불가 — 같은 시스템 안에서도 데이터마다
          요구하는 보장 강도가 다르고, 그걸 구분해 각각 알맞은 값을 치르는 것이 특급 설계자의
          일이에요.
        </p>
        <div className="tip">
          <b>fan-out 산수를 입에 붙이기</b> 300명 방에서 읽음·presence 이벤트를 전원에게
          그대로 중계하면 이벤트당 300배, 전원이 활동하면 틱당 9만 건짜리 부가 기능이 됩니다.
          설계 리뷰에서 "읽음 표시도 되나요?"라는 가벼운 질문에 이 산수를 보여 주며 batch·
          throttle·화면 볼 때만 구독 같은 절감책을 붙이는 것 — 그게 "네, 됩니다"보다 훨씬
          비싼 대답이에요.
        </div>
      </Section>

      <Section n={7} title="그룹 채팅과 대형 채널" sub="크기가 전략을 뒤집는다 — 그리고 악성 트래픽">
        <p>
          그룹 메시지를 참여자들에게 퍼뜨리는 방법은 근본적으로 둘입니다. 보낼 때 전원의
          수신함에 <strong>미리 복사해 두기(쓰기 fan-out·push)</strong>, 아니면 채널에 한 번만
          쓰고 <strong>읽는 사람이 가져가기(읽기 fan-out·pull)</strong>. 어느 쪽이 옳은가는
          방의 <strong>크기</strong>가 정합니다.
        </p>
        <Code>{CODE_FANOUT}</Code>
        <p>
          그리고 10만 명 "채널"을 자세히 보면, 그건 이미 채팅이 아니에요. 소수가 쓰고 다수가
          읽는 <strong>방송</strong> — 구조적으로 피드(feed)에 가깝습니다. 유명인 계정 하나가
          수백만 팔로워의 타임라인을 흔드는 문제와 정확히 같은 모양이죠. 이 지점은 다음
          훈련(뉴스피드와 타임라인)의 출발점이니, 오늘은 경계선만 정확히 그어 둡니다 —{' '}
          <strong>"참여자 수가 어떤 임계를 넘으면 push에서 pull로 전략을 바꾸겠습니다"</strong>가
          오늘 챙길 답이에요.
        </p>
        <p>
          마지막으로, 열린 채팅에는 반드시 <strong>악성 사용자</strong>가 옵니다. 우리가
          traderbox 라이브 채팅에서 직접 겪고 막았던 것 그대로예요 — <strong>무인증 쓰기
          차단</strong>(익명에게 쓰기를 열면 스팸 봇의 놀이터가 된다), 유저·IP 단위{' '}
          <strong>레이트리밋</strong>(초당 메시지 수 제한), 그리고 신고·차단 동선. 설계
          다이어그램에 이 층이 없으면 "운영해 본 적 없는 설계"라는 인상을 줍니다. 방어층은
          연결의 입구(인증된 WebSocket 핸드셰이크)와 메시지의 입구(발행 직전 검사) 양쪽에
          세우세요.
        </p>
        <div className="tip">
          <b>임계값은 외우는 숫자가 아니다</b> "1천 명부터 pull"은 정답이 아니라 예시예요.
          진짜 답은 <strong>쓰기 비용과 읽기 비용이 교차하는 지점을 자기 워크로드로 계산해
          정한다</strong>는 태도입니다. 면접에서도 숫자보다 "이렇게 재서 정하겠다"는 방법을
          말하는 쪽이 살아남아요.
        </div>
      </Section>

      <Section n={8} title="전체 조립과 방어 연습" sub="다이어그램 완성 → 장애 주입 → 트레이드오프로 답하기">
        <p>
          이제 부품을 전부 책상 위에 올렸습니다. 조립하면 이렇게 돼요 — 로드밸런서 뒤에{' '}
          <strong>연결만 들고 진실은 밖에 두는</strong> 채팅 서버군, 그 뒤에 Valkey(Pub/Sub
          백본·세션 레지스트리·presence), 메시지 저장소, 오프라인 유저를 깨우는 푸시
          게이트웨이. 그리고 완성된 그림에는 반드시 <strong>장애를 주입</strong>해 봅니다.
          설계 리뷰의 후반전은 언제나 "이게 죽으면요?"거든요.
        </p>
        <Code>{CODE_ARCH}</Code>
        <p>
          장애 시나리오에서 가장 소중한 문장은 <strong>"저장이 진실이고, 방송은 가속이다"</strong>
          예요. Pub/Sub이 죽어도 저장소가 살아 있으면 유실은 없고 실시간성만 잠시 잃습니다 —
          커서 동기화가 따라잡아 주니까요. 반대로 저장을 건너뛰고 방송만 먼저 하면 빨라
          보이지만, 그 순간 "유실 불가" 약속이 깨집니다. 리전 단절 같은 큰 재해에서도 원칙은
          같아요. <strong>어떤 보장을 지키고 어떤 보장을 잠시 내려놓을지</strong>를 먼저
          정해 두면, 복구 절차는 그 결정을 따라 자연히 써집니다.
        </p>
        <p>
          말로만 끝내면 아까우니, 오늘의 백본을 <strong>손으로</strong> 만들어 봅시다. 채팅
          서버 2개 인스턴스를 Valkey Pub/Sub으로 묶고, 서로 다른 서버에 붙은 두 브라우저가
          대화하게 만드는 미니 실습이에요. 섹션 3의 그림이 40분 만에 눈앞에서 움직입니다.
        </p>
        <Code>{CODE_PRACTICE}</Code>
        <div className="tip">
          <b>설계 리뷰 답변 훈련</b> 리뷰어의 질문은 정답 확인이 아니라 <strong>트레이드오프
          감각 검사</strong>예요. "왜 Kafka가 아니라 Valkey Pub/Sub인가요?" → "실시간 fan-out엔
          지연이 낮은 Pub/Sub이 맞고, 유실 불가 보장은 저장소가 이미 지고 있어서 백본에
          로그까지 요구하지 않았습니다. 구독자 부재 시에도 메시지가 남아야 하는 요구가 생기면
          Kafka로 올립니다." — <strong>선택 + 근거 + 바뀌는 조건</strong> 3박자로 답하는 연습을
          이 코스의 모든 선택지에 대해 해 보세요.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>💬 여기까지 왔다면</h3>
          <p className="muted">
            요청↔응답의 세계(훈련 1)를 지나 <strong>상태 있는 장수명 연결</strong>의 세계를
            한 바퀴 돌았습니다. WebSocket을 고른 이유, Pub/Sub 백본으로 서버 사이를 잇는 법,
            채널 단위 단조 증가 ID 하나로 순서·페이지네이션·동기화·읽음을 전부 푸는 감각,
            "최소 한 번 + 멱등"이라는 전달 보장의 진짜 모양, 그리고 크기가 커지면 push에서
            pull로 전략이 뒤집힌다는 것까지. 그런데 마지막 문턱에서 우리는 봤죠 — 10만 명
            채널은 이미 채팅이 아니라 <strong>피드</strong>라는 걸. 소수가 쓰고 수백만이 읽는
            타임라인은 어떻게 만들어질까요? 다음{' '}
            <Link to="/learn/lv4-system-design-newsfeed"><strong>시스템 설계 훈련 3 —
            뉴스피드와 타임라인</strong></Link>에서, 오늘 그어 둔 경계선 너머를 이어서
            설계해 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
