// 이 파일이 하는 일: "React 상태와 이펙트 실전" 코스 — 입문에서 배운 useState를
// '상태 설계'의 관점으로 끌어올리고, 불변 업데이트·상태 끌어올리기·useEffect·데이터 페칭·
// 커스텀 훅까지 7개 섹션으로 안내하는 정적 학습 페이지. 프레이밍은 "상태를 다루는 손에서
// 상태를 설계하는 눈으로" — 실습은 우리 블로그 API에서 글 목록을 불러와 검색·필터가
// 실시간으로 동작하는 목록 화면을 만드는 것이다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

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

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

const CODE_UI_F_STATE = `UI = f(state)

state(데이터)  ──→  f(컴포넌트 함수)  ──→  UI(화면)

  count: 3          function Counter()      <p>3개</p>
  count: 4          같은 함수, 다시 실행       <p>4개</p>

핵심: React에서 화면은 "그때그때 손으로 고치는 것"이 아니라
"상태를 넣으면 나오는 계산 결과"예요. 화면을 바꾸고 싶으면
DOM을 만지는 게 아니라 상태를 바꿉니다. 나머지는 React의 몫.`;

const CODE_RENDER_FLOW = `// 상태가 바뀌면 React가 하는 일 — 순서대로 추적해 보기
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}개
    </button>
  );
}

// ① 클릭 → setCount(1) 호출. 이 순간 count가 바로 바뀌는 게 아니라
//    "다음 렌더링 때 1로 해 줘"라고 예약만 됩니다.
// ② React가 Counter 함수를 '처음부터 다시' 실행 — 이게 리렌더링.
//    이번엔 useState가 1을 돌려줘요.
// ③ 새 JSX(1개)와 이전 JSX(0개)를 비교해서, 달라진 텍스트만
//    실제 DOM에 반영합니다. 전체를 다시 그리지 않아요.`;

const CODE_DERIVED = `// 장바구니 — 무엇을 state로 두고, 무엇을 계산으로 둘 것인가
const [items, setItems] = useState([]);   // ① 진짜 원본 데이터 → state

// ② 아래는 전부 items에서 '파생'되는 값 → state가 아니라 그냥 계산
const totalCount = items.length;
const totalPrice = items.reduce((sum, it) => sum + it.price * it.qty, 0);
const isEmpty = items.length === 0;

// 렌더링될 때마다 다시 계산되니 items와 어긋날 방법이 없어요.
// state로 두는 순간부터는 "items가 바뀔 때마다 같이 고쳐 줘야 하는 짐"이 됩니다.`;

const CODE_DUP_BUG = `// 나쁜 예 — 같은 사실을 두 곳에 저장하면 언젠가 서로 어긋난다
const [items, setItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);   // items에서 파생 가능한데 state로!

function addItem(item) {
  setItems([...items, item]);
  setTotalPrice(totalPrice + item.price);   // 여기까진 기억했는데...
}

function removeItem(id) {
  setItems(items.filter((it) => it.id !== id));
  // 앗, totalPrice 갱신을 깜빡! → 화면의 합계가 실제와 어긋남.
  // 이런 버그는 조용히 숨어 있다가 한참 뒤에 발견돼요.
}

// 고치는 법: totalPrice state를 지우고, 렌더링 때 items에서 계산.
// "최소 상태" 원칙 — 저장은 원본만, 나머지는 계산으로.`;

const CODE_MUTATE_BUG = `// 왜 직접 수정하면 화면이 안 바뀔까?
const [user, setUser] = useState({ name: '미림', level: 1 });

// 나쁜 예 — 객체 내부를 직접 수정
function levelUpWrong() {
  user.level = 2;        // 값은 바뀌었지만...
  setUser(user);         // '같은 객체'를 넘김 → React: "이전이랑 같은데? 스킵"
}

// React는 성능을 위해 내용물을 다 뒤져 보지 않고
// "이전 객체 === 새 객체" 참조 비교만 합니다.
// 같은 상자를 다시 건네면, 안의 내용이 바뀌었어도 알아채지 못해요.

// 좋은 예 — 새 객체를 만들어 교체
function levelUpRight() {
  setUser({ ...user, level: 2 });   // 새 상자 → 참조가 다름 → 리렌더!
}`;

const CODE_IMMUTABLE = `// 배열 상태의 불변 업데이트 3종 세트 — 전부 '새 배열을 돌려주는' 메서드만 사용
const [todos, setTodos] = useState([]);

// 추가: push 대신 스프레드로 새 배열
setTodos([...todos, newTodo]);

// 삭제: splice 대신 filter
setTodos(todos.filter((t) => t.id !== targetId));

// 수정: 직접 대입 대신 map으로 "그 항목만 새 객체로 교체"
setTodos(todos.map((t) =>
  t.id === targetId ? { ...t, done: !t.done } : t
));

// 금지 목록(원본을 바꾸는 메서드): push, pop, splice, sort, reverse
// 허용 목록(새 배열을 주는 메서드): map, filter, slice, concat, [...spread]`;

const CODE_NESTED = `// 중첩 객체 업데이트 — 바깥부터 안쪽까지 '지나는 길목의 껍데기'를 전부 복사
const [profile, setProfile] = useState({
  name: '미림',
  settings: { theme: 'dark', noti: { email: true, push: false } },
});

// push 알림만 켜고 싶다면:
setProfile({
  ...profile,                          // ① 최상위 복사
  settings: {
    ...profile.settings,               // ② settings 복사
    noti: {
      ...profile.settings.noti,        // ③ noti 복사
      push: true,                      // ④ 바꿀 값만 교체
    },
  },
});

// 세 겹만 넘어가면 손이 아프죠? 그건 "상태가 너무 깊다"는 신호이기도 해요.
// 상태를 평평하게(flat) 쪼개거나, 나중에 배울 useReducer를 고려할 타이밍입니다.`;

const CODE_LIFT = `// 상태 끌어올리기 — 검색창과 목록이 같은 상태를 봐야 할 때
// 나쁜 구조: SearchBar 안에 query가 갇혀 있으면 PostList가 볼 수 없다.

// 좋은 구조: 두 형제가 함께 쓰는 '최소 공통 부모'가 상태의 주인이 된다.
function PostBoard() {
  const [query, setQuery] = useState('');            // 주인은 부모

  return (
    <div>
      <SearchBar query={query} onChange={setQuery} /> {/* 값 + 바꿀 권한 */}
      <PostList query={query} />                      {/* 값만 읽기 */}
    </div>
  );
}

function SearchBar({ query, onChange }) {
  return (
    <input value={query} onChange={(e) => onChange(e.target.value)} />
  );
}

// 데이터는 아래로(props), 변경 요청은 위로(콜백). React의 기본 물길이에요.`;

const CODE_EFFECT_DEPS = `// 의존성 배열 3형태 — "언제 다시 실행되는가"가 전부 다르다
useEffect(() => {
  console.log('매 렌더링마다 실행');
});                       // ① 배열 없음: 렌더링될 때마다 — 거의 안 씀

useEffect(() => {
  console.log('처음 화면에 나타날 때 딱 한 번');
}, []);                   // ② 빈 배열: 마운트 시 1회 — 초기 데이터 로드에 애용

useEffect(() => {
  console.log('query가 바뀔 때마다 실행');
}, [query]);              // ③ [query]: 첫 렌더 + query가 달라진 렌더 후

// 규칙: 이펙트 안에서 읽는 컴포넌트의 값(state·props)은
// 전부 의존성 배열에 넣는 게 원칙이에요. 빼먹으면 '옛날 값'을 보는 버그가 생깁니다.`;

const CODE_CLEANUP = `// 클린업 함수 — 이펙트가 벌인 일을 치우는 담당
useEffect(() => {
  const timer = setInterval(() => {
    console.log('1초마다 똑딱');
  }, 1000);

  return () => clearInterval(timer);   // ← 이게 클린업 함수
}, []);

// 클린업이 실행되는 정확한 시점 두 가지:
// ① 컴포넌트가 화면에서 사라질 때(언마운트)
// ② 의존성이 바뀌어 이펙트가 '다시' 실행되기 직전
//    (새 이펙트 실행 전에, 이전 이펙트의 클린업이 먼저!)
//
// 순서 예: query가 'a' → 'ab'로 바뀌면
//   'a' 이펙트의 클린업 실행 → 'ab' 이펙트 실행
// 이 순서 덕분에 타이머·구독이 중복으로 쌓이지 않아요.`;

const CODE_FETCH = `// 데이터 페칭의 정석 — 로딩·에러·데이터 3상태 + 레이스 컨디션 방어
function PostList({ query }) {
  const [posts, setPosts] = useState([]);      // ① 데이터
  const [loading, setLoading] = useState(true); // ② 로딩 중인가
  const [error, setError] = useState(null);     // ③ 실패했는가

  useEffect(() => {
    let ignore = false;                // 이번 요청이 '유효한지' 표시하는 깃발
    setLoading(true);
    setError(null);

    fetch('/api/posts?query=' + encodeURIComponent(query))
      .then((res) => {
        if (!res.ok) throw new Error('서버 응답 오류: ' + res.status);
        return res.json();
      })
      .then((data) => {
        if (!ignore) setPosts(data);   // 깃발이 안 꽂혔을 때만 반영
      })
      .catch((err) => {
        if (!ignore) setError(err.message);
      })
      .finally(() => {
        if (!ignore) setLoading(false);
      });

    return () => { ignore = true; };   // 클린업: 이전 요청에 "무시해" 깃발
  }, [query]);                          // query가 바뀔 때마다 다시 요청

  if (loading) return <p>불러오는 중...</p>;
  if (error) return <p>문제가 생겼어요: {error}</p>;
  if (posts.length === 0) return <p>결과가 없어요.</p>;
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}`;

const CODE_RACE = `// 레이스 컨디션 — 빠른 연속 요청이 뒤바뀌어 도착하는 사고
사용자가 타이핑: 'r' → 're' → 'react'   (요청 3발이 연달아 출발)

출발 순서:  요청A('r')   요청B('re')   요청C('react')
도착 순서:  요청C 먼저!  ...한참 뒤 요청A가 도착

방어 없이 setPosts를 그냥 하면?
→ 마지막에 도착한 요청A('r')의 결과가 화면을 덮어써서
  검색어는 'react'인데 목록은 'r' 검색 결과인 어긋남이 생겨요.

ignore 깃발의 동작:
query가 바뀌면 → 이전 이펙트의 클린업이 먼저 실행(섹션 5에서 배운 순서!)
→ 이전 요청의 ignore가 true가 됨
→ 늦게 도착해도 if (!ignore) 문턱에서 걸러짐. 최신 요청만 화면에 반영.`;

const CODE_USEFETCH = `// 커스텀 훅 — 반복되는 페칭 로직을 useFetch 하나로 추출
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;
    setLoading(true);
    setError(null);

    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error('서버 응답 오류: ' + res.status);
        return res.json();
      })
      .then((json) => { if (!ignore) setData(json); })
      .catch((err) => { if (!ignore) setError(err.message); })
      .finally(() => { if (!ignore) setLoading(false); });

    return () => { ignore = true; };
  }, [url]);                      // url이 바뀌면 자동으로 다시 요청

  return { data, loading, error };  // 쓰는 쪽에 3상태를 세트로 전달
}

// 사용하는 쪽 — 컴포넌트가 이렇게 얇아진다
function PostList({ query }) {
  const { data: posts, loading, error } =
    useFetch('/api/posts?query=' + encodeURIComponent(query));

  if (loading) return <p>불러오는 중...</p>;
  if (error) return <p>문제가 생겼어요: {error}</p>;
  return <ul>{(posts ?? []).map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. 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: 'UI = f(state)' },
  { n: 2, label: '무엇을 state로' },
  { n: 3, label: '불변 업데이트' },
  { n: 4, label: '상태 끌어올리기' },
  { n: 5, label: 'useEffect' },
  { n: 6, label: '데이터 페칭' },
  { n: 7, label: '커스텀 훅' },
];

export default function Lv2ReactStateEffectsPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 중급 과정</div>
        <h1>⚛️ React 상태와<br />이펙트 실전</h1>
        <p>
          입문에서 <span className="icode">useState</span>로 버튼 카운터를 만들어 봤다면, 이제
          질문이 바뀝니다. "상태를 <strong>어떻게 쓰는가</strong>"에서 "무엇을 상태로 두고,
          누가 주인이 되고, 바깥 세상과는 어떻게 동기화하는가" — 즉 <strong>상태를
          '설계'하는 단계</strong>로요. 불변 업데이트, useEffect 데이터 페칭, 커스텀 훅까지,
          실제 서비스 코드에서 매일 마주치는 패턴을 한 줄씩 손에 익혀 봅시다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 75분</span>
          <span className="chip">실습: 블로그 글 목록 + 실시간 검색·필터</span>
          <span className="chip">선수 지식: JS 기초 문법 · React 입문(useState)</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="상태가 UI를 만든다" sub="UI = f(state) — 렌더링 모델부터 다지기">
        <p>
          React를 관통하는 문장은 딱 하나예요. <strong>UI = f(state)</strong>. 화면(UI)은
          상태(state)를 컴포넌트 함수(f)에 넣었을 때 나오는 <strong>계산 결과</strong>라는
          뜻입니다. 화면을 바꾸고 싶으면 화면을 직접 만지는 게 아니라 <strong>상태를
          바꿉니다.</strong> 그러면 React가 함수를 다시 실행해서 새 화면을 계산해 내죠.
        </p>
        <Code>{CODE_UI_F_STATE}</Code>
        <p>
          그럼 <span className="icode">setCount</span>를 호출하는 순간 정확히 무슨 일이
          벌어질까요? 많은 분들이 "변수가 바로 바뀐다"고 생각하는데, 실제로는{' '}
          <strong>예약 → 재실행 → 비교 반영</strong>의 세 단계를 거칩니다.
        </p>
        <Code>{CODE_RENDER_FLOW}</Code>
        <p>
          여기서 중요한 건 <strong>②의 '함수를 처음부터 다시 실행한다'</strong>는 사실이에요.
          컴포넌트 안에 쓴 일반 변수·계산식은 렌더링마다 새로 만들어지고, 오직{' '}
          <span className="icode">useState</span>에 맡긴 값만 렌더링 사이에서 살아남습니다.
          이 그림이 머리에 있어야 다음 섹션들 — "왜 파생값은 state가 아닌가", "왜 직접 수정하면
          안 되는가" — 가 전부 자연스럽게 이어져요.
        </p>
        <div className="tip">
          <b>관점의 승격</b> 입문 때는 "useState를 호출할 줄 아는 것"이 목표였다면, 이번
          과정의 목표는 <strong>"이 화면에는 어떤 상태가 몇 개 필요한가"를 먼저 종이에 그릴
          수 있는 것</strong>이에요. 코드는 그 설계의 결과물일 뿐입니다.
        </div>
      </Section>

      <Section n={2} title="무엇을 state로 둘 것인가" sub="파생 가능한 값은 저장하지 말고 계산하라">
        <p>
          상태 설계의 제1원칙: <strong>state는 최소한으로.</strong> 판별법은 간단해요. "이 값,
          다른 state나 props에서 <strong>계산으로 만들어 낼 수 있나?</strong>" — 만들어 낼 수
          있다면 그건 state가 아니라 <strong>그냥 변수(계산)</strong>여야 합니다. 장바구니로
          연습해 볼게요.
        </p>
        <Code>{CODE_DERIVED}</Code>
        <p>
          "합계도 자주 쓰니까 state로 두면 편하지 않나?"라는 생각이 들 수 있어요. 그런데 같은
          사실을 두 곳(<span className="icode">items</span>와{' '}
          <span className="icode">totalPrice</span>)에 저장하는 순간, 둘을{' '}
          <strong>영원히 손으로 동기화해야 하는 계약</strong>이 생깁니다. 그리고 사람은 반드시
          한 번은 깜빡하죠.
        </p>
        <Code>{CODE_DUP_BUG}</Code>
        <p>
          정리하면, state 후보는 이 세 관문을 통과해야 해요. <strong>① 시간이 지나며
          변하는가?</strong>(안 변하면 상수) <strong>② 부모가 props로 주는 값인가?</strong>
          (그렇다면 부모의 것) <strong>③ 다른 값에서 계산 가능한가?</strong>(가능하면 계산으로).
          셋을 다 통과한 값만 <span className="icode">useState</span>에 올립니다.
        </p>
        <div className="tip">
          <b>실습 미리보기</b> 이번 실습의 목록 화면에서 state는 딱 넷이에요 —{' '}
          <span className="icode">posts</span>(서버가 준 원본),{' '}
          <span className="icode">query</span>(검색어), <span className="icode">category</span>
          (선택된 카테고리), 로딩·에러. "필터링된 목록"은? state가 아니라{' '}
          <strong>posts에서 매 렌더링마다 계산</strong>합니다. 이 구분이 이번 과정의 절반이에요.
        </div>
      </Section>

      <Section n={3} title="객체·배열 상태의 불변 업데이트" sub="고치지 말고, 복사해서 교체하라">
        <p>
          숫자·문자열은 <span className="icode">setCount(count + 1)</span>처럼 새 값을 주면
          그만인데, <strong>객체와 배열</strong>은 함정이 있어요. 내부를 직접 고치면{' '}
          <strong>화면이 안 바뀝니다.</strong> React가 게을러서가 아니라, 변경 감지를{' '}
          <strong>참조 비교(이전 것과 같은 상자인가?)</strong>로 하기 때문이에요.
        </p>
        <Code>{CODE_MUTATE_BUG}</Code>
        <p>
          그래서 규칙은 하나로 수렴합니다. <strong>"고치지 말고, 복사본을 만들어 바꿀 부분만
          교체한 뒤, 통째로 새로 건넨다."</strong> 배열에서 자주 쓰는 패턴 세 가지를 손에
          익혀 두세요 — 추가는 스프레드, 삭제는 filter, 수정은 map입니다.
        </p>
        <Code>{CODE_IMMUTABLE}</Code>
        <p>
          객체가 여러 겹 중첩돼 있으면, 바꾸려는 값까지 <strong>지나가는 길목의 껍데기를 전부
          복사</strong>해야 해요. 안 그러면 중간 껍데기는 여전히 옛 상자라서, 그 아래를 보는
          컴포넌트가 변경을 놓칠 수 있거든요.
        </p>
        <Code>{CODE_NESTED}</Code>
        <div className="warn">
          <b>조용한 버그의 대표 주자</b> <span className="icode">todos.push(...)</span> 후{' '}
          <span className="icode">setTodos(todos)</span> — 에러도 경고도 없이 그냥 화면만 안
          바뀝니다. "분명 값은 바뀌었는데 렌더가 안 돼요"의 90%가 이 직접 수정이에요. 콘솔에
          찍어 보면 값은 바뀌어 있어서 더 헷갈리죠. 의심되면 먼저 물어보세요 — "나 지금 원본을
          고치고 있나, 새것을 만들고 있나?"
        </div>
      </Section>

      <Section n={4} title="상태 끌어올리기와 컴포넌트 분리" sub="상태의 주인은 '함께 쓰는 최소 공통 부모'">
        <p>
          실습 화면을 그려 보면 바로 부딪히는 문제예요. 검색창(<span className="icode">SearchBar</span>)에
          입력한 검색어를, <strong>형제</strong>인 목록(<span className="icode">PostList</span>)이
          알아야 필터링을 하죠. 그런데 React의 데이터는 <strong>부모에서 자식으로, 한
          방향</strong>으로만 흐릅니다. 형제끼리는 직접 대화할 수 없어요.
        </p>
        <p>
          해법이 <strong>상태 끌어올리기(lifting state up)</strong>입니다. 상태를 두 형제가{' '}
          <strong>함께 쓰는 가장 가까운 공통 부모</strong>로 올리고, 부모가 값은 props로
          내려 주고 바꿀 권한은 콜백으로 내려 주는 거예요.
        </p>
        <Code>{CODE_LIFT}</Code>
        <p>
          "상태의 주인을 어디에 둘까"의 기준은 늘 같아요. <strong>그 상태를 읽거나 바꾸는
          컴포넌트들을 전부 모아, 그들을 모두 품는 가장 낮은(가까운) 부모.</strong> 그보다
          높이 올리면 관계없는 컴포넌트까지 리렌더링되고, 낮게 두면 필요한 쪽이 못 봅니다.
        </p>
        <div className="tip">
          <b>props가 깊어질 때의 신호</b> 부모 → 자식 → 손자 → 증손자로 같은 props를 그냥
          '전달만' 하는 층이 3~4개를 넘으면(이걸 <strong>props drilling</strong>이라 불러요),
          컴포넌트 경계를 다시 그리거나 Context 같은 도구를 고려할 타이밍입니다. 다만 이번
          실습 규모(부모 하나 + 형제 둘)에서는 끌어올리기만으로 충분해요 — 도구는 문제가
          생긴 다음에 꺼내는 겁니다.
        </div>
      </Section>

      <Section n={5} title="useEffect 바로 알기" sub="이펙트는 '렌더링 밖 세상'과의 동기화">
        <p>
          지금까지의 이야기는 전부 <strong>렌더링 안</strong>의 세계였어요. state가 들어가면
          JSX가 나오는 순수한 계산이죠. 그런데 실제 앱은 <strong>렌더링 밖 세상</strong>과도
          손잡아야 합니다 — 서버에 데이터를 요청하고, 타이머를 걸고, 브라우저 이벤트를 구독하는
          일들요. 이 "바깥 세상과의 동기화"를 담당하는 게 <span className="icode">useEffect</span>
          예요. <strong>렌더링이 화면에 반영된 뒤</strong>에 실행됩니다.
        </p>
        <p>
          useEffect의 성격을 결정하는 건 두 번째 인자, <strong>의존성 배열</strong>입니다.
          세 가지 형태의 차이를 정확히 구분하세요 — 면접에서도, 실무 버그에서도 단골이에요.
        </p>
        <Code>{CODE_EFFECT_DEPS}</Code>
        <p>
          이펙트가 <strong>벌인 일</strong>(타이머, 구독, 진행 중인 요청)은 누군가 치워야
          합니다. 그 담당이 이펙트가 <strong>return하는 클린업 함수</strong>예요. 실행 시점이
          두 곳이라는 걸 꼭 기억하세요 — 사라질 때 한 번만이 아닙니다.
        </p>
        <Code>{CODE_CLEANUP}</Code>
        <div className="warn">
          <b>이펙트 남용 주의</b> "state가 바뀌면 다른 state를 계산해서 set하는 이펙트"는
          대부분 잘못된 설계예요. 그건 섹션 2에서 배운 <strong>파생값 — 렌더링 중 계산</strong>
          으로 풀어야 합니다. 이펙트는 어디까지나 <strong>React 바깥</strong>(서버·타이머·DOM
          API)과 만날 때만 꺼내는 도구예요. "이거 바깥 세상이랑 관련 있나?"를 먼저 물어보세요.
        </div>
      </Section>

      <Section n={6} title="데이터 페칭 실전" sub="로딩·에러·데이터 3상태 + 레이스 컨디션 방어">
        <p>
          드디어 실습의 심장부입니다. 우리가 만든 <strong>블로그 API</strong>에서 글 목록을
          불러와 봅시다. 서버 요청은 "성공해서 데이터가 온다"만 있는 게 아니에요.{' '}
          <strong>기다리는 중</strong>일 수도, <strong>실패</strong>할 수도 있죠. 그래서 페칭은
          늘 <strong>로딩·에러·데이터 3상태를 한 세트</strong>로 관리합니다.
        </p>
        <Code>{CODE_FETCH}</Code>
        <p>
          코드에 낯선 장치가 하나 있죠 — <span className="icode">ignore</span> 깃발. 이게 없으면
          어떤 사고가 나는지 봅시다. 사용자가 검색창에 빠르게 타이핑하면 요청이 연발로 나가는데,
          네트워크는 <strong>출발 순서대로 도착한다는 보장이 없어요.</strong>
        </p>
        <Code>{CODE_RACE}</Code>
        <p>
          섹션 5에서 배운 클린업의 실행 순서("새 이펙트 전에 이전 클린업 먼저")가 여기서
          빛납니다. <span className="icode">query</span>가 바뀔 때마다 이전 요청의 깃발이
          먼저 꽂히니, <strong>늦게 도착한 옛 응답은 조용히 무시</strong>되고 항상 최신 요청만
          화면에 반영돼요. 덤으로, 컴포넌트가 화면에서 사라진 뒤 도착한 응답도 같은 깃발에
          걸러지므로 "언마운트된 컴포넌트에 setState" 경고도 함께 사라집니다.
        </p>
        <div className="tip">
          <b>실습 조립법</b> 이제 부품이 다 모였어요. <strong>① 부모</strong>가{' '}
          <span className="icode">query</span>·<span className="icode">category</span> state를
          들고(섹션 4) → <strong>②</strong> 그 값이 바뀌면 이펙트가 API를 다시 호출하고(이번
          섹션) → <strong>③</strong> 카테고리 필터링은 받아 온 <span className="icode">posts</span>
          에서 <strong>계산</strong>으로(섹션 2) → <strong>④</strong> 목록 갱신은 전부 새 배열
          교체로(섹션 3). 과정 전체가 화면 하나에 조립되는 순간입니다.
        </div>
      </Section>

      <Section n={7} title="커스텀 훅으로 재사용" sub="반복되는 페칭 로직을 useFetch로 추출">
        <p>
          섹션 6의 페칭 코드, 잘 보면 <strong>어느 화면에서든 똑같이 반복될 모양</strong>이에요.
          글 목록에서도, 글 상세에서도, 댓글에서도 — 3상태 선언, fetch, ignore 깃발, 클린업이
          매번 복붙되겠죠. React는 이런 <strong>state가 얽힌 로직의 재사용</strong>을 위해{' '}
          <strong>커스텀 훅</strong>이라는 길을 열어 뒀습니다. <span className="icode">use</span>로
          시작하는 이름의 함수를 만들고, 그 안에서 다른 훅을 쓰면 돼요.
        </p>
        <Code>{CODE_USEFETCH}</Code>
        <p>
          효과를 보세요. <span className="icode">PostList</span>가 <strong>"무엇을 보여 줄지"만
          남고 "어떻게 가져올지"는 사라졌습니다.</strong> 컴포넌트는 얇아지고, 페칭 로직은 한
          곳에서 관리되니 버그 수정도 한 번이면 되죠. 한 가지 주의 — 커스텀 훅은 로직을
          재사용하는 것이지 <strong>state를 공유하는 게 아니에요.</strong> useFetch를 쓰는
          컴포넌트마다 각자의 독립된 data·loading·error를 가집니다.
        </p>
        <p>
          마지막으로 <strong>훅의 규칙</strong>. 모든 훅(useState·useEffect·커스텀 훅)은
          컴포넌트나 커스텀 훅의 <strong>최상위에서만</strong> 호출해야 해요.{' '}
          <span className="icode">if</span>문·반복문·중첩 함수 안은 금지입니다. React는 훅이{' '}
          <strong>호출된 순서</strong>로 "첫 번째 useState는 이 값, 두 번째는 저 값"을
          기억하거든요. 조건문 안에서 호출하면 렌더링마다 순서가 달라져서, React가 state를
          엉뚱한 훅에 배달하게 됩니다.
        </p>
        <div className="warn">
          <b>이름의 약속</b> <span className="icode">use</span> 접두어는 장식이 아니에요.
          "이 함수 안에서 훅이 호출된다"는 신호라서, React와 ESLint가 이 이름을 보고 훅의
          규칙 위반을 잡아 줍니다. 훅을 쓰는 함수엔 반드시 <span className="icode">use</span>를
          붙이고, 훅을 안 쓰는 유틸 함수엔 붙이지 마세요.
        </div>
        <div className="tip">
          <b>한 걸음 더</b> 실무에선 이 useFetch의 발전형이 바로 TanStack Query(React Query)
          같은 라이브러리예요. 캐싱·재시도·백그라운드 갱신까지 얹은 '아주 잘 만든 커스텀 훅
          모음'이라고 보면 됩니다. 직접 useFetch를 만들어 본 지금이라면, 그 라이브러리 문서가
          전혀 다르게 읽힐 거예요.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>⚛️ 여기까지 왔다면</h3>
          <p className="muted">
            이제 여러분은 상태를 '쓰는' 사람에서 '설계하는' 사람으로 한 계단 올라왔어요.
            최소 상태 고르기, 복사해서 교체하는 불변 업데이트, 최소 공통 부모로 끌어올리기,
            바깥 세상과 동기화하는 useEffect, 레이스 컨디션까지 막아 내는 페칭, 그리고 그
            전부를 useFetch 한 줄로 접어 넣는 커스텀 훅까지 — 실전 React의 뼈대를 손에
            쥐었습니다. 그런데 화면이 하나뿐인 앱은 없죠. 글 목록에서 글 상세로,{' '}
            <strong>URL이 바뀌며 화면이 전환되는 진짜 서비스의 구조</strong>가 다음 주제예요.
            다음 <Link to="/learn/lv2-react-routing"><strong>React Router로 멀티 페이지
            SPA</strong></Link> 강좌에서, 오늘 만든 목록 화면에 상세 페이지를 이어 붙여 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
