본문으로 바로가기
제품데스크톱 도구TASTECAD도면 확인TASTEFILES파일 관리TASTEFTP파일 전송TASTESSH원격 작업공간TASTEZIP압축과 해제개발 도구TASTECODERust 통합 개발 환경TASTESTUDIO통합 개발·테스트 환경TASTEETL데이터 통합(ETL)서버TASTEREMOTE원격 데스크톱 서버TASTEGEO공간정보 서버TASTEMAIL메일 작업공간시작하기다운로드문서구매모든 제품
마켓플레이스
소식
개발소식오늘의 개발 소식개발 문서프로그래밍 언어RustTypeScriptJavaScriptPythonGoC#JavaCC++KotlinDart프런트엔드 프레임워크Next.jsReactVue.js · NuxtSvelteKitAngular서버 프레임워크Spring BootDjangoFastAPIASP.NET CoreNestJS · Express앱 · 데스크톱 프레임워크TauriFlutterReact NativeElectron알고리즘알고리즘 기초알고리즘 핵심알고리즘 심화실무 알고리즘
커뮤니티프로젝트개발 로그프롬프트AI 도구 이슈쇼케이스커뮤니티 안내
협업기술 전수홈페이지 구축데스크톱 앱 구축릴리스·결제 파이프라인판매 파트너문의
서비스원격 데스크톱설치 패키지 커스텀
TASTEDEV
제품

데스크톱 도구

TASTECAD복잡한 도면을 가볍게 열다TASTEFILES필요한 파일만, 가장 빠르게TASTEFTP파일이 오가는 가장 단정한 길TASTESSH서버와 나 사이, 더 단단한 연결TASTEZIP더 빠르게 묶고, 더 안전하게 풀다

개발 도구

TASTECODERust 앱을 편집 · 빌드 · 디버깅 · 테스트까지 한 창에서TASTESTUDIO편집부터 원격 테스트와 승인 기반 AI 수정까지TASTEETL값을 보며 데이터를 옮깁니다

서버

TASTEREMOTE우리 회사 서버로 운영하는 원격 데스크톱TASTEGEOGeoServer 호환 공간정보 서버를 Rust로 다시 세웁니다TASTEMAIL메일의 주도권을 다시 내 손에

시작하기

다운로드모든 제품 최신 판문서제품 사용법구매구독 · 영구 라이선스모든 제품모든 제품 한눈에
모든 제품
마켓플레이스소식
개발

소식

오늘의 개발 소식언어 · 프레임워크 · GitHub · AI 공식 발표개발 문서언어 · 프레임워크 · 알고리즘 안내서

프로그래밍 언어

RustTypeScriptJavaScriptPythonGoC#JavaCC++KotlinDart

프레임워크

프런트엔드 프레임워크Next.jsReactVue.js · NuxtSvelteKitAngular
서버 프레임워크Spring BootDjangoFastAPIASP.NET CoreNestJS · Express
앱 · 데스크톱 프레임워크TauriFlutterReact NativeElectron

알고리즘

알고리즘 기초복잡도 분석 · 정렬 · 탐색 · 기본 자료구조 · 트리와 힙알고리즘 핵심그래프 탐색 · 최단 경로와 최소 신장 트리 · 동적 계획법 · 탐욕 알고리즘 · 분할 정복 · 백트래킹알고리즘 심화문자열 알고리즘 · 수학과 정수론 · 비트 연산 · 구간 트리 · 유니온 파인드실무 알고리즘코딩 면접 풀이 전략 · LRU 캐시 · 레이트 리미터 · 일관된 해시
개발 문서 전체 보기
커뮤니티
프로젝트빌더와 작업 발견개발 로그결정과 시행착오프롬프트재사용 가능한 지시AI 도구 이슈Claude · Codex · Gemini 문제와 우회법쇼케이스완성된 결과커뮤니티 안내공개하고 함께 만드는 방식
협업
기술 전수Rust·egui 네이티브 앱 만들기홈페이지 구축10개 언어, 계정, 문서·다운로드데스크톱 앱 구축Rust·egui, WebView 없이릴리스·결제 파이프라인서명 · MSI·deb·rpm·AppImage · 해시판매 파트너소개하고 결제액의 30% 받기문의구축 · 제휴 · 투자 문의
서비스
원격 데스크톱브라우저로 내 컴퓨터에 접속설치 패키지 커스텀필요한 기능만, 설정은 미리
구매
KO
한국어EnglishDeutschEspañolFrançaisItalianoPortuguês (Brasil)日本語简体中文繁體中文
로그인
로그인
TASTEDEV

작은 도구부터, 더 좋은 개발까지.

support@tastedev.netTASTEDEV 후원하기
제품제품군구매문서다운로드마켓플레이스고객 지원
커뮤니티프로젝트개발 로그프롬프트AI 도구 이슈쇼케이스
정책개인정보처리방침이용약관구독 약관환불 정책커뮤니티 운영 정책
계정로그인가입하기
방문자
오늘
012345678901234567890123456789012345678901234567890123456789
어제
012345678901234567890123456789012345678901234567890123456789
누적 조회
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
상호
지엑스소프트
대표자
최영구
사업자등록번호
853-44-00368
주소
인천광역시 미추홀구 경인로 351 9층 V24
전화
+82-10-4404-1523
이메일
whitecalvin@tastedev.net
개인정보 보호책임자
최영구

© 2026 TASTEDEV제품 정보는 현재 검증된 개발 소스를 기준으로 합니다.출시·고도화 중

React 문서
React
React 시작하기프로젝트 구조핵심 개념데이터와 상태 관리테스트빌드와 배포
토론

React 안내서 · 4/6

데이터와 상태 관리

앱이 커지면 상태를 어디에 둘지, 서버 데이터를 어떻게 불러올지, 폼 입력을 어떻게 다룰지가 중요해집니다. 이 장에서는 상태 끌어올리기, Context, 리듀서, 데이터 불러오기, 폼을 차례로 다룹니다.

상태 끌어올리기

두 컴포넌트가 같은 데이터를 써야 한다면, 상태를 둘의 가장 가까운 공통 부모로 옮기고 props로 값과 변경 함수를 내려 줍니다. 이렇게 하면 데이터의 출처가 한 곳(single source of truth)으로 유지됩니다.

import { useState } from "react";

function TemperatureInput({ value, onChange }: { value: number; onChange: (v: number) => void }) {
  return <input type="number" value={value} onChange={(e) => onChange(Number(e.target.value))} />;
}

export function Thermostat() {
  const [celsius, setCelsius] = useState(20);
  return (
    <div>
      <TemperatureInput value={celsius} onChange={setCelsius} />
      <p>화씨 {(celsius * 9) / 5 + 32}도</p>
    </div>
  );
}

Context로 깊이 전달하기

테마, 로그인 사용자, 언어처럼 여러 단계 아래의 컴포넌트가 함께 쓰는 값을 매번 props로 넘기면 번거롭습니다(prop drilling). createContext로 Context를 만들고 위에서 값을 제공하면, 아래의 어떤 컴포넌트든 useContext로 읽을 수 있습니다. 최신 React에서는 Context 자체를 Provider로 쓸 수 있으며, 이전 코드에서는 <ThemeContext.Provider> 형태도 볼 수 있습니다.

import { createContext, useContext } from "react";

type Theme = "light" | "dark";
const ThemeContext = createContext<Theme>("light");

export function App({ theme }: { theme: Theme }) {
  return (
    <ThemeContext value={theme}>
      <Toolbar />
    </ThemeContext>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={`toolbar toolbar-${theme}`}>현재 테마: {theme}</div>;
}

Context 값이 바뀌면 그 값을 읽는 모든 컴포넌트가 다시 렌더링됩니다. 자주 바뀌는 큰 상태를 하나의 Context에 몰아넣기보다 용도별로 나누는 편이 좋습니다.

useReducer로 상태 변경 모으기

상태를 바꾸는 방법이 여러 가지라면 useReducer로 변경 규칙을 한 함수에 모읍니다. 컴포넌트는 "무엇이 일어났는지"를 담은 액션만 보내고(dispatch), 리듀서가 새 상태를 계산합니다. TypeScript의 구별된 유니온으로 액션을 정의하면 빠진 경우를 잡아낼 수 있습니다.

import { useReducer } from "react";

type Todo = { id: number; text: string; done: boolean };
type Action =
  | { type: "added"; text: string }
  | { type: "toggled"; id: number };

function todosReducer(todos: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "added":
      return [...todos, { id: Date.now(), text: action.text, done: false }];
    case "toggled":
      return todos.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
  }
}

export function useTodos() {
  const [todos, dispatch] = useReducer(todosReducer, []);
  return { todos, add: (text: string) => dispatch({ type: "added", text }), dispatch };
}

리듀서와 Context를 함께 쓰면 작은 앱에서는 별도 라이브러리 없이도 전역 상태를 관리할 수 있습니다. 더 큰 앱에서는 Zustand, Redux Toolkit 같은 상태 관리 라이브러리를 검토합니다.

서버 데이터 불러오기

Effect 안에서 fetch를 직접 호출할 수 있지만, 응답 순서가 뒤바뀌는 경쟁 상태를 막으려면 정리 함수에서 요청을 취소해야 합니다.

import { useEffect, useState } from "react";

type User = { id: number; name: string };

export function UserName({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then((res) => res.json() as Promise<User>)
      .then(setUser)
      .catch((err) => {
        if (err.name !== "AbortError") console.error(err); // 취소는 무시
      });
    return () => controller.abort();
  }, [userId]);

  return <span>{user ? user.name : "불러오는 중…"}</span>;
}

캐시, 재시도, 로딩 · 오류 상태, 화면 복귀 시 갱신까지 직접 구현하기는 어렵습니다. 그래서 실제 앱에서는 TanStack Query 같은 라이브러리를 많이 씁니다. main.tsx에서 QueryClientProvider로 앱을 감싼 뒤 다음처럼 씁니다.

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

type Todo = { id: number; title: string };

export function Todos() {
  const queryClient = useQueryClient();
  const { data, isPending, error } = useQuery({
    queryKey: ["todos"],
    queryFn: async (): Promise<Todo[]> => (await fetch("/api/todos")).json(),
  });
  const addTodo = useMutation({
    mutationFn: (title: string) =>
      fetch("/api/todos", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ title }),
      }),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
  });

  if (isPending) return <p>불러오는 중…</p>;
  if (error) return <p>오류: {error.message}</p>;
  return (
    <>
      <ul>{data.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
      <button onClick={() => addTodo.mutate("새 할 일")}>추가</button>
    </>
  );
}

폼 다루기

입력값을 상태에 묶는 제어 컴포넌트는 입력할 때마다 검사하거나 값을 바꿀 때 유용합니다. 최신 React에서는 <form action={...}>에 함수를 넘기고 useActionState로 결과와 진행 상태를 받는 방식도 쓸 수 있습니다.

import { useActionState } from "react";

type State = { error: string | null };

async function subscribe(_prev: State, formData: FormData): Promise<State> {
  const email = String(formData.get("email") ?? "");
  if (!email.includes("@")) return { error: "이메일 형식이 아닙니다." };
  await fetch("/api/subscribe", { method: "POST", body: formData });
  return { error: null };
}

export function SubscribeForm() {
  const [state, formAction, isPending] = useActionState(subscribe, { error: null });
  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <button disabled={isPending}>{isPending ? "보내는 중…" : "구독"}</button>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}

입력 항목이 많고 검사 규칙이 복잡하면 React Hook Form 같은 폼 라이브러리를 함께 쓰는 것도 좋은 선택입니다.

정리

  • 여러 컴포넌트가 쓰는 상태는 가장 가까운 공통 부모로 끌어올립니다.
  • 깊이 전달할 값은 Context로, 복잡한 변경 규칙은 useReducer로 다룹니다.
  • Effect에서 데이터를 불러올 때는 정리 함수로 요청을 취소하고, 실제 앱에서는 TanStack Query 같은 라이브러리를 검토합니다.
  • 폼은 제어 컴포넌트나 form action과 useActionState로 다룹니다.
이 장에 대해 질문이 있나요?
이전 장핵심 개념다음 장테스트

댓글 0개

인기 댓글순최신순

로그인 · 로그인하면 댓글을 남길 수 있습니다.