React
3줄 요약
- React의 핵심은 컴포넌트, 상태, 렌더링을 이용해 “현재 상태라면 화면은 이래야 한다”를 선언적으로 표현하는 것이다.
- 백엔드 개발자가 자주 헷갈리는 지점은 state snapshot, stale closure,
useEffect의존성, list key, controlled input이다. - 서버 상태는 React state만으로 버티기보다 TanStack Query 같은 서버 상태 도구로 cache와 invalidation을 관리하는 편이 안전하다.
핵심 정리
- 컴포넌트는 UI 조각이면서 상태와 이벤트를 묶는 단위다.
- props는 부모가 내려주는 입력이고 state는 컴포넌트가 렌더링 사이에 기억하는 값이다.
- 렌더링은 함수가 다시 실행되어 새로운 UI 설명을 만들고, React가 이전 결과와 비교해 DOM 반영을 결정하는 과정이다.
useEffect는 렌더링 결과를 외부 시스템과 동기화하는 도구이지, 모든 계산을 넣는 곳이 아니다.- list key는 성능 힌트가 아니라 item identity를 보존하는 기준이다.
백엔드 개발자가 헷갈리는 지점
setState직후 변수 값이 바뀐다고 생각하기 쉽다.- 판단 기준은 state가 렌더링 시점의 snapshot이라는 점이다.
- 다음 값 계산에는 functional update를 사용한다.
useEffect를 “컴포넌트가 로드될 때 실행되는 곳”으로만 이해하기 쉽다.- 판단 기준은 effect가 외부 시스템과 동기화하는 코드인지다.
- props나 state에서 계산 가능한 값은 effect state로 복사하지 않는다.
- key를 index로 둬도 화면이 잘 나오니 괜찮다고 느끼기 쉽다.
- 판단 기준은 목록이 reorder, insert, delete될 가능성이다.
- 안정적인 서버 id나 client id를 key로 둔다.
개인 프로젝트 기준
- 화면 local state는 가까운 컴포넌트에 둔다.
- 서버 데이터, cache, refetch는 TanStack Query로 관리한다.
- form은 React Hook Form을 쓰고, 작은 controlled input은 직접 관리한다.
- React 문법 함정은 React stale closure는 왜 생길까?와 [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#useeffect-dependency-array-실수는-왜-자주-생길까|
useEffectdependency array 실수는 왜 자주 생길까?]]를 함께 본다.
확인 질문
- React state는 언제의 값을 보여주는가?
- 현재 렌더링 시점의 snapshot을 보여준다.
useEffect는 무엇을 위한 도구인가?- 렌더링 결과를 네트워크, DOM API, subscription 같은 외부 시스템과 동기화하기 위한 도구다.
- list key는 무엇을 보존하는가?
- 목록 item의 identity를 보존한다.
01. 컴포넌트 상태 렌더링 상세
이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- React에서 상태 변경은 왜 즉시 변수 값을 바꾸는 방식이 아닌가?
- 컴포넌트 분리 기준은 파일 크기인가, 책임과 상태 경계인가?
- derived state와 서버 상태 복사는 왜 위험해질 수 있는가?
개요
React는 DOM을 직접 조작하기보다 상태에 따른 UI 결과를 선언한다. 백엔드 개발자에게 익숙한 “객체의 필드를 바꾸면 그 객체가 바뀐다”는 감각과 달리, React state는 렌더링 시점의 snapshot이다. 상태 변경은 현재 함수 안의 변수를 즉시 바꾸는 명령이 아니라 다음 렌더링을 예약하는 요청이다.
원리
컴포넌트 함수는 현재 props와 state를 입력으로 받아 UI 설명을 반환한다. state가 바뀌면 React는 컴포넌트를 다시 호출한다. 이때 기존 state 객체를 직접 mutate하면 참조가 유지되어 React와 memoization 도구가 변경을 추적하기 어렵고, UI와 데이터가 어긋날 수 있다.
컴포넌트 분리는 단순히 줄 수가 많아서 하는 작업이 아니다. 상태를 소유하는 곳, 재사용되는 UI, 도메인 의미가 있는 조각, 테스트하고 싶은 상호작용을 기준으로 나눈다.
코드로 보는 핵심 예시
잘못된 state mutation은 화면 갱신을 흔들리게 만든다.
type Project = {
id: number;
title: string;
pinned: boolean;
};
function ProjectList({ initialProjects }: { initialProjects: Project[] }) {
const [projects, setProjects] = useState(initialProjects);
function togglePinned(id: number) {
setProjects((prev) =>
prev.map((project) =>
project.id === id
? { ...project, pinned: !project.pinned }
: project,
),
);
}
return (
<ul>
{projects.map((project) => (
<li key={project.id}>
<button onClick={() => togglePinned(project.id)}>
{project.pinned ? "고정 해제" : "고정"}
</button>
{project.title}
</li>
))}
</ul>
);
}derived state는 state로 복사하지 않고 렌더링 중 계산한다.
function ProjectSummary({ projects }: { projects: Project[] }) {
const pinnedCount = projects.filter((project) => project.pinned).length;
return <p>고정된 프로젝트 {pinnedCount}개</p>;
}백엔드와 맞춰야 할 지점
- 목록 item에는 안정적인
id가 있어야 React key로 사용할 수 있다. - 상태 badge, 권한별 disabled 상태, 삭제 가능 여부는 API가 판단할지 프론트가 계산할지 정한다.
- 화면에서 필요한 summary 값을 API가 내려줄지 프론트가 계산할지 데이터 크기와 일관성 기준으로 합의한다.
개인 프로젝트 기준
- 서버에서 온 목록은 TanStack Query cache에 두고, 체크박스 선택 상태 같은 UI 상태는 별도 local state로 둔다.
- 깊은 nested state가 많아지면 상태를 평평하게 만들거나 reducer를 쓴다.
- 컴포넌트는 “데이터 가져오기”, “상태 조작”, “표시” 책임이 너무 섞이지 않게 나눈다.
- 컴포넌트를 나눌 때는 파일 수보다 상태를 누가 소유하는지와 테스트할 사용자 행동이 어디인지 먼저 본다.
실전 팁
- state setter에 이전 값이 필요하면
setState((prev) => next)형태를 쓴다. - list key는 index보다 서버 id나 client id를 사용한다.
- props에서 계산 가능한 값을 다시 state로 복사하지 않는다.
- 관련 함정은 객체와 배열 mutation 때문에 React 화면이 안 바뀌는 이유는 무엇일까?, React list key를 index로 두면 언제 문제가 될까?, [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#setstate-직후-값이-바로-바뀌었다고-생각하면-왜-틀릴까|
setState직후 값이 바로 바뀌었다고 생각하면 왜 틀릴까?]]에서 다시 본다.
위험 신호!
- 서버 데이터를 여러 컴포넌트 state에 복사해 서로 다른 값이 된다.
push,splice, 직접 대입으로 state 배열을 수정한다.- 계산 가능한 값을 effect로 다시 state에 저장한다.
- list key를 습관적으로 index로 둔다.
확인 질문
확인 질문
- React state를 직접 mutate하면 왜 위험한가?
- React가 참조 변경을 기준으로 갱신을 판단하므로 변경을 감지하지 못하거나 이전 렌더링과 어긋날 수 있다.
- 컴포넌트 분리 기준은 무엇인가?
- 책임, 상태 소유권, 재사용성, 도메인 의미, 테스트 가능성을 기준으로 나눈다.
- derived state를 남발하면 어떤 문제가 생기는가?
- 원본 props/state와 복사된 state가 서로 달라질 수 있다.
참고 문서
02. useEffect 이벤트 디버깅 상세
이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
useEffect는 언제 필요하고 언제 필요하지 않은가?- stale closure와 dependency array 문제를 어떻게 디버깅하는가?
- API 호출을 effect에 직접 넣을 때 무엇을 조심해야 하는가?
개요
useEffect는 React에서 가장 자주 오용되는 Hook이다. 이름 때문에 “렌더링 후 무언가 실행하는 곳”으로 보이지만, 핵심은 외부 시스템과의 동기화다. 외부 시스템은 네트워크, timer, subscription, 브라우저 API, 직접 DOM 조작처럼 React 렌더링만으로 표현할 수 없는 대상이다. 반대로 props와 state에서 계산 가능한 값은 effect가 아니라 렌더링 중 계산하는 편이 단순하다.
원리
React 컴포넌트 함수 안의 값은 렌더링마다 새로 만들어진다. effect 안의 callback은 특정 렌더링 시점의 값을 캡처한다. dependency array는 effect가 어떤 렌더링 값에 의존하는지를 React와 lint 도구에게 알려준다. 의존성을 빼면 오래된 값을 쓰고, 매번 새로 만들어지는 객체를 넣으면 effect가 의도보다 자주 반복된다.
코드로 보는 핵심 예시
API 검색 요청은 dependency와 취소를 같이 다룬다.
import { z } from "zod";
const itemSchema = z.object({
id: z.number(),
name: z.string(),
});
const itemListSchema = z.array(itemSchema);
type Item = z.infer<typeof itemSchema>;
function SearchPage({ keyword }: { keyword: string }) {
const [state, setState] = useState<
| { status: "idle" | "loading" }
| { status: "success"; items: Item[] }
| { status: "error"; message: string }
>({ status: "idle" });
useEffect(() => {
const controller = new AbortController();
async function search() {
setState({ status: "loading" });
try {
const response = await fetch(`/api/items?q=${encodeURIComponent(keyword)}`, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error("검색에 실패했습니다.");
}
const body: unknown = await response.json();
const items = itemListSchema.parse(body);
setState({ status: "success", items });
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
return;
}
setState({ status: "error", message: "검색 결과를 불러오지 못했습니다." });
}
}
search();
return () => controller.abort();
}, [keyword]);
}이전 state 기반 업데이트는 stale closure를 피하기 위해 functional update를 사용한다.
function NotificationBell() {
const [count, setCount] = useState(0);
useEffect(() => {
const unsubscribe = subscribeNotification(() => {
setCount((prev) => prev + 1);
});
return unsubscribe;
}, []);
return <span>{count}</span>;
}백엔드와 맞춰야 할 지점
- 검색 API는 debounce를 고려해 서버 부하와 rate limit 정책을 정한다.
- 오래 걸리는 API는 취소되어도 서버 작업을 계속할지, request id로 무시할지 정한다.
- API 에러는 retry 가능한 실패와 즉시 사용자 조치가 필요한 실패를 구분한다.
개인 프로젝트 기준
- 단순 mount 시 1회 호출은 effect로 구현할 수 있지만, 목록/상세/검색 서버 상태는 TanStack Query를 우선 검토한다.
useEffectdependency lint는 끄지 않는다.- effect 안에서 쓰는 함수가 매번 바뀌어 문제가 되면 위치를 옮기거나
useCallback의 필요성을 검토한다. - 요청 취소는 UI 반영을 막는 장치이고, 서버 작업까지 취소되는지는 API 구현과 인프라에 따라 다를 수 있다.
실전 팁
- effect 없이 렌더링 중 계산 가능한 값은 effect로 만들지 않는다.
useEffect(() => setX(derive(a)), [a])패턴은 derived state 냄새일 수 있다.- fetch는 cleanup에서
AbortController로 취소한다. - 관련 함정은 React stale closure는 왜 생길까?와 [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#useeffect-dependency-array-실수는-왜-자주-생길까|
useEffectdependency array 실수는 왜 자주 생길까?]]를 본다.
위험 신호!
- dependency lint를 끈다.
- effect 안에서 state를 바꾸고 그 state가 다시 dependency가 되어 무한 반복된다.
- cleanup 없는 subscription, interval, fetch가 쌓인다.
- 서버 상태 관리 전체를 effect와
useState로 직접 구현한다.
확인 질문
확인 질문
useEffect가 필요한 대표 상황은 무엇인가?
- React 렌더링 결과를 네트워크, timer, subscription, DOM API 같은 외부 시스템과 동기화할 때다.
- stale closure는 왜 생기는가?
- callback이 생성된 렌더링 시점의 값을 캡처하기 때문이다.
- dependency array에서 값을 빼면 어떤 문제가 생기는가?
- effect가 최신 값을 보지 못하고 오래된 값으로 동작할 수 있다.