React 실무 UI 흐름 해설팩 01

자료 유형: 무료 실무 해설팩

React 기본 문법은 알지만 폼, state, API 화면에서 자주 막히는 학습자를 위한 실무 해설팩입니다. 컴포넌트 문법보다 사용자가 입력하고, 제출하고, 실패하고, 다시 시도하는 흐름을 안정적으로 설계하는 데 초점을 맞춥니다.

이 팩의 약속

이 자료는 JSX 문법 요약이 아니다. 실제 화면에서 사용자 행동이 들어왔을 때 React state와 UI가 어떻게 움직여야 하는지 설명한다.

  • controlled input 없이 입력값이 흩어지는 문제
  • button onClick만으로 제출을 처리해 Enter/모바일 제출을 놓치는 문제
  • 로딩/성공/실패 상태가 섞이는 문제
  • API 실패 화면에 복구 액션이 없는 문제

예제 1. controlled input

연결 문제: https://nst21c.com/?task=reactInputPreview#labApp

흔한 실패 코드

function SearchBox() {
  let keyword = "";

  return (
    <input onChange={(event) => {
      keyword = event.target.value;
    }} />
  );
}

변수는 바뀌지만 React는 화면을 다시 그리지 않는다. 검색어 미리보기, 필터, 폼 검증처럼 입력값과 화면이 연결되어야 하는 곳에서 문제가 생긴다.

개선 코드

function SearchBox() {
  const [keyword, setKeyword] = useState("");

  return (
    <section>
      <input
        value={keyword}
        onChange={(event) => setKeyword(event.target.value)}
        placeholder="검색어"
      />
      <p>현재 검색어: {keyword || "없음"}</p>
    </section>
  );
}

실무 화면 예

  • 검색창
  • 관리자 필터
  • 문의 폼
  • 입력값 미리보기

코드 리뷰 체크포인트

  • 입력값의 기준이 React state인가?
  • valueonChange가 함께 있는가?
  • 빈 값일 때 화면이 어색하지 않은가?

예제 2. form onSubmit과 검증

연결 문제: https://nst21c.com/?task=reactFormValidation#labApp

흔한 실패 코드

function EmailForm() {
  const [email, setEmail] = useState("");

  return (
    <div>
      <input value={email} onChange={(event) => setEmail(event.target.value)} />
      <button onClick={() => submit(email)}>신청하기</button>
    </div>
  );
}

버튼 클릭만 처리하면 Enter 제출, 모바일 키보드 제출, 접근성 흐름을 놓친다. 이메일 형식이 틀렸을 때 사용자가 왜 실패했는지도 알기 어렵다.

개선 코드

function EmailForm() {
  const [email, setEmail] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  const emailError =
    email.trim().length === 0
      ? "이메일을 입력해주세요."
      : !email.includes("@")
        ? "이메일 형식을 확인해주세요."
        : "";

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (emailError || isSubmitting) return;

    setIsSubmitting(true);
    try {
      await submit(email.trim());
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={email}
        onChange={(event) => setEmail(event.target.value)}
        aria-invalid={Boolean(emailError)}
      />
      {emailError && <p role="alert">{emailError}</p>}
      <button type="submit" disabled={Boolean(emailError) || isSubmitting}>
        {isSubmitting ? "신청 중" : "신청하기"}
      </button>
    </form>
  );
}

실무 화면 예

  • 뉴스레터 신청
  • 문의 폼
  • 로그인/회원가입
  • 결제 전 정보 입력

코드 리뷰 체크포인트

  • 제출 로직이 form onSubmit에 있는가?
  • preventDefault가 있는가?
  • 제출 중 중복 클릭을 막는가?
  • 에러 메시지가 사용자 행동을 안내하는가?

예제 3. API 화면 상태 분리

연결 문제: https://nst21c.com/?task=reactLoadingState#labApp

흔한 실패 코드

function UserList({ loading, error, users }) {
  if (loading) return <p>불러오는 중</p>;
  if (error) return <p>오류</p>;
  return <ul>{users.map((user) => <li>{user.name}</li>)}</ul>;
}

usersundefined이면 바로 깨진다. 또 빈 배열과 실패 상태가 분리되지 않으면 사용자는 “데이터가 없는지, 오류인지” 알 수 없다.

개선 코드

type UserListState =
  | { status: "loading" }
  | { status: "success"; users: User[] }
  | { status: "empty" }
  | { status: "error"; message: string };

function UserList({ state }: { state: UserListState }) {
  if (state.status === "loading") return <p>불러오는 중입니다.</p>;
  if (state.status === "error") return <p role="alert">{state.message}</p>;
  if (state.status === "empty") return <p>조건에 맞는 사용자가 없습니다.</p>;

  return (
    <ul>
      {state.users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

실무 화면 예

  • 상품 목록
  • 사용자 프로필
  • 검색 결과
  • 관리자 대시보드 카드

코드 리뷰 체크포인트

  • 로딩, 성공, 빈 결과, 실패가 분리되어 있는가?
  • 성공 상태가 아닐 때 목록을 렌더링하지 않는가?
  • 목록 key가 안정적인 ID인가?

예제 4. 실패 상태와 재시도

연결 문제: https://nst21c.com/?task=reactApiRetryState#labApp

흔한 실패 코드

if (error) {
  return <p>오류가 발생했습니다.</p>;
}

실패 메시지만 보여주면 사용자는 다음 행동을 알 수 없다. 실무 화면에서 실패 상태는 끝이 아니라 복구 흐름의 시작이다.

개선 코드

function ErrorPanel({ onRetry, retrying }: { onRetry: () => void; retrying: boolean }) {
  return (
    <section role="alert">
      <h2>데이터를 불러오지 못했습니다</h2>
      <p>네트워크 상태를 확인한 뒤 다시 시도해주세요.</p>
      <button type="button" onClick={onRetry} disabled={retrying}>
        {retrying ? "다시 불러오는 중" : "다시 시도"}
      </button>
    </section>
  );
}

실무 화면 예

  • 네트워크 실패 복구
  • 서버 오류 후 재요청
  • 인증 만료 후 다시 불러오기
  • 저장 실패 후 재시도

코드 리뷰 체크포인트

  • 실패 상태에 복구 액션이 있는가?
  • 재시도 중 중복 요청을 막는가?
  • 개발자용 에러 문구를 그대로 노출하지 않는가?

5일 복습 루틴

1일차: controlled input을 만들고 입력값의 기준을 설명한다. 2일차: form validation을 풀고 onSubmitpreventDefault를 설명한다. 3일차: loading/error/empty/success 상태를 화면 단위로 나눈다. 4일차: retry 버튼을 붙이고 중복 요청을 막는다. 5일차: 네 예제를 하나의 문의 신청 화면으로 합친다.

이 팩 다음에 보낼 후속 안내

React 팩을 받은 사용자는 TypeScript 상태 모델링과 연결하면 더 큰 가치를 느낀다. 후속 메일에서는 ApiState<T>를 React 화면에 연결하는 미니 프로젝트로 안내한다.