React 개발 핵심 정리
React를 처음 배우면 컴포넌트와 JSX부터 접하게 된다.
하지만 React를 제대로 이해하려면 문법보다 상태가 어떻게 UI로 연결되고 다시 상태 변경으로 돌아오는지를 먼저 이해하는 것이 중요하다.
React의 핵심은 상태를 기반으로 UI를 선언하는 것이다.
State -> Render -> UI -> User Event -> State Update -> Render
개발자가 DOM을 직접 찾아 수정하는 대신 현재 상태에서 어떤 UI가 만들어져야 하는지를 작성한다. React는 상태가 변경되면 새로운 UI를 계산하고 필요한 DOM 변경을 반영한다.
React의 기본 구조
React 애플리케이션은 여러 컴포넌트를 조합해서 하나의 UI를 만든다.
function App() {
return (
<div>
<Header />
<Main />
<Footer />
</div>
);
}
컴포넌트 관계는 다음처럼 구성된다.
App -> Header / Main / Footer
Main 내부에 다시 여러 컴포넌트가 있다면:
Main -> Sidebar / Content
이런 식으로 컴포넌트를 조합해 전체 UI를 구성한다.
컴포넌트를 나누는 목적은 무조건 화면을 잘게 쪼개는 것이 아니다. 책임이 분리되거나 재사용되는 UI를 독립적인 단위로 만드는 것이 핵심이다.
JSX
React에서는 JavaScript 코드 안에 HTML과 비슷한 문법을 작성한다.
function User({ name }) {
return (
<div>
<h2>{name}</h2>
<p>Developer</p>
</div>
);
}
이 문법이 JSX다.
JSX가 브라우저에서 그대로 실행되는 것은 아니다. 빌드 과정에서 JavaScript가 이해할 수 있는 형태로 변환된다.
JSX -> JavaScript -> React Element -> React Rendering -> DOM
JSX의 {} 안에는 JavaScript 표현식을 넣을 수 있다.
<p>{user.name}</p>
조건부 렌더링도 가능하다.
{isLogin && <Profile />}
리스트는 일반적으로 map()을 사용한다.
users.map(user => (
<User key={user.id} user={user} />
))
JSX는 단순한 HTML 대체 문법이 아니라 JavaScript와 UI 표현을 하나의 코드 안에서 결합하는 문법이라고 보는 편이 정확하다.
Component
컴포넌트는 React UI를 구성하는 기본 단위다.
function Button() {
return <button>Click</button>;
}
함수형 컴포넌트는 입력을 받아 UI를 반환하는 형태로 생각할 수 있다.
Props / State -> Component -> UI
컴포넌트의 책임이 지나치게 커지거나 동일한 UI가 여러 곳에서 사용된다면 분리할 수 있다.
반대로 단순히 코드 몇 줄을 별도의 컴포넌트로 만드는 것이 항상 좋은 것은 아니다.
컴포넌트 분리의 기준은 변경 이유와 책임의 분리에 두는 것이 좋다.
Props
Props는 부모 컴포넌트가 자식 컴포넌트에 데이터를 전달하는 방법이다.
function User({ name }) {
return <div>{name}</div>;
}
function App() {
return <User name="John" />;
}
데이터 흐름은 기본적으로 부모에서 자식 방향이다.
Parent -> Props -> Child
Props는 자식 컴포넌트가 직접 변경하는 데이터가 아니다.
부모가 값을 관리하고 자식은 전달받은 값을 이용해 UI를 계산한다.
이러한 단방향 데이터 흐름은 React 구조를 이해하는 핵심이다.
State
Props가 외부에서 전달되는 값이라면 State는 컴포넌트가 관리하는 상태다.
가장 기본적인 형태는 useState()다.
const [count, setCount] = useState(0);
상태를 직접 변경하는 것이 아니라 Setter를 사용한다.
setCount(count + 1);
상태 변경은 새로운 렌더링으로 이어진다.
setState() -> State Update -> Render -> UI Update
따라서 다음과 같이 직접 변경하는 것은 올바른 방식이 아니다.
count++;
React가 관리하는 State를 변경할 때는 제공되는 상태 업데이트 방식을 사용해야 한다.
Rendering
React의 핵심은 결국 Rendering이다.
컴포넌트는 현재 Props와 State를 기반으로 UI를 계산한다.
function Counter() {
const [count, setCount] = useState(0);
return <h1>{count}</h1>;
}
count가 변경되면 React는 컴포넌트를 다시 실행해 새로운 결과를 계산한다.
count = 0 -> <h1>0</h1>
count = 1 -> <h1>1</h1>
여기서 중요한 것은 컴포넌트가 다시 실행되는 것과 실제 DOM 전체가 다시 만들어지는 것은 다르다는 점이다.
React는 새로운 결과를 계산하고 기존 결과와 비교한 뒤 필요한 변경을 실제 DOM에 반영한다.
Reconciliation
이전 렌더링 결과와 새로운 렌더링 결과를 비교하고 실제 UI 변경을 결정하는 과정과 관련된 개념이 Reconciliation이다.
예를 들어 이전 UI가:
A / B / C
였다가 새로운 UI가:
A / B / D
가 되었다면 전체 DOM을 다시 만들 필요가 없다.
React는 변경된 부분을 파악하고 필요한 DOM 업데이트를 수행한다.
이 과정에서 리스트의 key가 중요하다.
Key
리스트를 렌더링할 때 React는 각 요소를 식별할 수 있어야 한다.
users.map(user => (
<User
key={user.id}
user={user}
/>
))
key는 단순히 경고를 제거하기 위한 값이 아니다.
React가 이전 렌더링의 요소와 새로운 렌더링의 요소를 연결하고 리스트의 변경을 효율적으로 처리하는 데 사용된다.
가능하면 데이터 자체의 고유 ID를 사용하는 것이 좋다.
key={user.id}
배열의 index를 Key로 사용하는 것은 리스트의 삽입, 삭제, 정렬 변경 등이 발생할 때 문제가 생길 수 있으므로 주의해야 한다.
Event
React에서는 사용자 입력을 JSX 이벤트 핸들러와 연결한다.
<button onClick={handleClick}>
Click
</button>
일반적인 흐름은 다음과 같다.
User Input -> Event Handler -> setState() -> Render -> UI Update
예를 들어 입력창의 값을 State로 관리할 수 있다.
function Input() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
이처럼 React State가 Input 값을 관리하는 방식을 Controlled Component라고 한다.
useEffect
useEffect는 React에서 자주 오해하는 Hook이다.
단순히 컴포넌트가 화면에 나타났을 때 실행하는 코드라고 이해하면 부족하다.
useEffect의 핵심 목적은 렌더링으로 계산되는 UI와 외부 시스템을 동기화하는 것이다.
대표적인 사용 사례는 다음과 같다.
- 네트워크 연결
- 브라우저 이벤트 구독
- 타이머
- 외부 라이브러리 연동
- 외부 시스템과의 동기화
예를 들어 타이머를 등록할 수 있다.
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
반환하는 함수는 Cleanup이다.
Effect -> External System -> Cleanup -> Effect 재실행 / Component Unmount
렌더링 과정에서 계산할 수 있는 값을 굳이 useEffect에서 계산하는 것은 피하는 것이 좋다.
Effect는 외부 시스템과의 동기화가 필요한 경우를 우선적으로 생각하면 된다.
useMemo와 useCallback
React에는 성능 최적화를 위한 useMemo와 useCallback도 있다.
useMemo는 계산 결과를 메모이제이션한다.
const result = useMemo(() => {
return calculate(data);
}, [data]);
useCallback은 함수를 메모이제이션한다.
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
하지만 둘 다 무조건 사용해야 하는 Hook은 아니다.
성능 문제 확인 -> 병목 측정 -> Memoization 적용
실제 병목이 없는데 모든 계산과 함수에 useMemo, useCallback을 적용하면 코드 복잡성만 증가할 수 있다.
Context
컴포넌트가 깊게 중첩되면 Props를 여러 단계에 걸쳐 전달해야 하는 상황이 생긴다.
App -> A -> B -> C -> D
D가 App의 데이터를 필요로 한다면 중간 컴포넌트가 실제로 사용하지 않는 Props까지 전달해야 할 수 있다.
App -> Props -> A -> Props -> B -> Props -> C -> Props -> D
Context는 특정 컴포넌트 트리 범위에서 데이터를 공유할 수 있게 한다.
const ThemeContext = createContext(null);
사용하는 쪽에서는:
const theme = useContext(ThemeContext);
대표적으로 Theme, 현재 사용자, 언어 설정, 애플리케이션 설정 등에 사용할 수 있다.
다만 Context를 모든 전역 상태에 사용하는 것은 적절하지 않다. Context 값이 변경되면 해당 값을 구독하는 컴포넌트에 렌더링 영향이 발생할 수 있기 때문이다.
State의 위치
React에서는 State를 어디에 둘 것인지가 중요하다.
두 컴포넌트가 동일한 State를 필요로 한다면 공통 부모로 State를 올릴 수 있다.
Parent -> State -> A / B
이를 Lifting State Up이라고 한다.
기본적인 기준은 해당 State를 필요로 하는 가장 가까운 공통 부모에 두는 것이다.
State를 무조건 최상위 컴포넌트까지 올리면 관리 범위가 불필요하게 커진다.
State의 종류
실제 React 애플리케이션에서는 모든 상태를 하나의 State로 생각하면 안 된다.
대표적으로 다음과 같이 구분할 수 있다.
UI State / Server State / Form State / URL State
예를 들어 Modal의 열림 여부는 UI State다.
isModalOpen -> UI State
서버에서 가져온 사용자 목록은 Server State다.
Server -> Network -> Cache -> React UI
Server State에는 캐싱, 재요청, Loading, Error, Stale Data 등의 문제가 함께 발생한다.
따라서 규모가 있는 애플리케이션에서는 서버 데이터를 전용 Query 라이브러리로 관리하는 경우가 많다.
핵심은 상태의 성격에 따라 관리 방법을 다르게 가져가는 것이다.
Custom Hook
React에서는 반복되는 상태 로직을 Custom Hook으로 분리할 수 있다.
function useCounter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(value => value + 1);
};
return {
count,
increment,
};
}
컴포넌트에서는 다음처럼 사용할 수 있다.
function Counter() {
const { count, increment } = useCounter();
return (
<button onClick={increment}>
{count}
</button>
);
}
Custom Hook의 목적은 UI 자체를 재사용하는 것이 아니다.
React의 상태 관리와 관련된 로직을 재사용하는 것이다.
Routing
SPA에서는 URL에 따라 다른 화면을 보여줘야 한다.
React에서는 React Router와 같은 라우팅 라이브러리를 사용할 수 있다.
URL -> Router -> Route -> Component
예를 들어:
/ -> Home
/users -> UserList
/users/123 -> UserDetail
/settings -> Settings
라우팅은 단순한 화면 전환뿐만 아니라 URL Parameter, Query String, Nested Route, Navigation 등을 관리하는 역할도 한다.
Form
React Form은 크게 Controlled 방식과 Uncontrolled 방식으로 나눠서 생각할 수 있다.
Controlled 방식에서는 React State가 입력값을 관리한다.
const [name, setName] = useState('');
<input
value={name}
onChange={e => setName(e.target.value)}
/>
Form이 복잡해지면 모든 Input을 직접 State로 관리하는 대신 전용 Form 라이브러리를 사용할 수도 있다.
여기서 중요한 것은 라이브러리보다 Form State를 어디에서 어떻게 관리할 것인가다.
React와 TypeScript
실제 React 프로젝트에서는 TypeScript를 함께 사용하는 경우가 많다.
Props의 데이터 구조를 명확하게 정의할 수 있다.
interface UserProps {
id: number;
name: string;
}
function User({ id, name }: UserProps) {
return <div>{name}</div>;
}
State에도 타입을 지정할 수 있다.
const [user, setUser] = useState<User | null>(null);
React가 TypeScript를 필수로 요구하는 것은 아니지만 프로젝트 규모가 커질수록 컴포넌트 간 데이터 계약을 명확하게 만들 수 있다는 장점이 크다.
컴포넌트 설계
컴포넌트 하나가 지나치게 많은 책임을 가지고 있으면 유지보수가 어려워진다.
예를 들어:
UserPage -> API Request / Data Transform / Form / Modal / Table / Pagination / State
이런 구조는 시간이 지날수록 복잡해질 가능성이 높다.
책임에 따라 분리하면 다음과 같이 구성할 수 있다.
UserPage -> UserList / UserForm / UserModal / Pagination
데이터 처리나 복잡한 상태 로직은 Custom Hook이나 별도의 데이터 계층으로 분리할 수 있다.
Component -> Custom Hook -> Service / Query -> API
하지만 컴포넌트를 작게 만드는 것 자체가 목표는 아니다.
변경 이유가 다른 코드와 재사용되는 코드를 적절하게 분리하는 것이 핵심이다.
React의 성능
React에서 성능 문제를 단순히 렌더링 횟수로 판단하면 안 된다.
렌더링은 React의 정상적인 동작이다.
실제로 확인해야 하는 것은 어떤 컴포넌트가 자주 렌더링되는지, 렌더링 과정에서 무거운 계산이 발생하는지, 실제 DOM 변경이 많은지, 네트워크나 이미지 처리 등이 병목인지다.
대표적인 원인은 다음과 같다.
불필요한 State Update / 넓은 State 범위 / 큰 List / 무거운 계산 / 과도한 Context Update / 불필요한 Effect
React DevTools Profiler 등을 사용해 실제 병목을 측정한 뒤 최적화하는 것이 좋다.
성능 문제 -> 측정 -> 병목 확인 -> 최적화 -> 재측정
추측으로 memo, useMemo, useCallback을 남발하는 것보다 측정 기반으로 접근하는 것이 효율적이다.
React 애플리케이션의 전체 구조
규모가 있는 React 애플리케이션을 단순화하면 다음과 같은 구조로 볼 수 있다.
React App -> Router / UI -> Component -> Hook / Query -> State / Server State -> API
조금 더 세분화하면:
React App -> Page -> Component -> Custom Hook -> Query / Service -> API
상태는 성격에 따라 분리한다.
State -> UI State / Server State / Form State / URL State
그리고 컴포넌트 사이의 기본 데이터 흐름은 다음과 같다.
Parent -> Props -> Child
사용자 입력은 상태 변경으로 연결된다.
User Input -> Event Handler -> State Update -> Render -> UI
React의 핵심 흐름
React를 하나의 흐름으로 압축하면 결국 다음 구조다.
State -> Render -> UI -> User Event -> State Update -> Render
컴포넌트 간 데이터는 기본적으로 단방향으로 흐른다.
Parent -> Props -> Child
서버 데이터를 사용하는 경우에는 데이터 계층이 추가된다.
Component -> Hook / Query -> API -> Server
외부 시스템과 동기화가 필요하면 Effect가 개입한다.
Render -> Effect -> External System -> Cleanup
애플리케이션 규모가 커지면 상태의 종류와 책임을 분리한다.
UI State / Server State / Form State / URL State
정리
React를 이해할 때 JSX 문법이나 Hook 목록부터 외우는 것보다 State와 UI의 관계를 먼저 잡는 것이 중요하다.
가장 핵심적인 구조는 다음이다.
State -> Render -> UI -> Event -> State Update -> Render
Component는 UI를 구성하고, Props는 부모에서 자식으로 데이터를 전달한다.
State는 UI를 결정하는 변경 가능한 데이터를 관리하고, 상태가 변경되면 React가 다시 렌더링한다.
useEffect는 외부 시스템과 React를 동기화할 때 사용하며, Context는 특정 컴포넌트 트리 범위에서 데이터를 공유한다.
Custom Hook은 상태 로직을 재사용하기 위한 도구이고, Query나 별도의 데이터 계층은 서버 상태를 관리하는 데 활용할 수 있다.
결국 React에서 중요한 질문은 문법이 아니다.
어떤 상태를 어디에서 관리하는가.
그 상태를 어떤 컴포넌트가 사용하는가.
상태가 변경됐을 때 어떤 UI가 다시 계산되는가.
이 세 가지를 이해하면 Props, State, Rendering, Effect, Context, Hook, Router 같은 기능들이 서로 독립적인 개념이 아니라 하나의 상태 기반 UI 시스템 안에서 연결되어 있다는 것을 이해할 수 있다.