React
React.js
Facebook이 개발한 UI 라이브러리. 컴포넌트 기반, Virtual DOM으로 효율적 렌더링. 가장 인기있는 프론트엔드 도구.
React.js
Facebook이 개발한 UI 라이브러리. 컴포넌트 기반, Virtual DOM으로 효율적 렌더링. 가장 인기있는 프론트엔드 도구.
React는 2013년 Facebook(현 Meta)이 공개한 JavaScript UI 라이브러리로, 현재 가장 널리 사용되는 프론트엔드 도구입니다. "Learn Once, Write Anywhere" 철학으로 웹(React), 모바일(React Native), 데스크톱(Electron + React)까지 확장됩니다. 선언적(Declarative) 프로그래밍 방식으로 UI의 상태만 정의하면 React가 DOM 업데이트를 자동 처리합니다.
React의 핵심은 컴포넌트(Component)와 Virtual DOM입니다. 컴포넌트는 UI를 독립적이고 재사용 가능한 단위로 분리하며, props로 데이터를 전달받고 state로 내부 상태를 관리합니다. Virtual DOM은 메모리상의 가상 DOM 트리로, 상태 변경 시 이전 Virtual DOM과 비교(Diffing)하여 최소한의 실제 DOM 업데이트만 수행합니다.
React 16.8에서 도입된 Hooks는 함수형 컴포넌트에서 상태 관리와 생명주기를 사용할 수 있게 해주는 혁신적 기능입니다. useState, useEffect, useContext, useReducer, useMemo, useCallback, useRef 등이 있습니다. Hooks 도입 후 클래스 컴포넌트는 거의 사용되지 않으며, 함수형 컴포넌트가 표준이 되었습니다.
React 18(2022)에서는 Concurrent Rendering, Automatic Batching, Transitions, Suspense for Data Fetching이 도입되었습니다. React 19(2024)에서는 Server Components, Actions, use() 훅이 추가되어 풀스택 React 개발이 가능해졌습니다. Next.js, Remix와 같은 메타 프레임워크와 함께 사용하면 SSR, SSG, ISR 등 다양한 렌더링 전략을 활용할 수 있습니다.
// React Hooks를 활용한 함수형 컴포넌트
import { useState, useEffect, useCallback, useMemo } from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface UserListProps {
filterRole?: string;
}
export function UserList({ filterRole }: UserListProps) {
// 상태 관리
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
// 데이터 페칭 - useEffect
useEffect(() => {
const controller = new AbortController();
async function fetchUsers() {
try {
setLoading(true);
const response = await fetch('/api/users', {
signal: controller.signal
});
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setUsers(data);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchUsers();
// Cleanup function - 컴포넌트 언마운트 시 요청 취소
return () => controller.abort();
}, [filterRole]); // filterRole이 변경되면 다시 실행
// 메모이제이션 - useMemo (비용이 큰 계산)
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]);
// 콜백 메모이제이션 - useCallback (자식에게 전달하는 함수)
const handleDelete = useCallback(async (userId: number) => {
if (!confirm('정말 삭제하시겠습니까?')) return;
await fetch(`/api/users/${userId}`, { method: 'DELETE' });
setUsers(prev => prev.filter(u => u.id !== userId));
}, []);
// 조건부 렌더링
if (loading) return <div className="loading">로딩 중...</div>;
if (error) return <div className="error">에러: {error}</div>;
return (
<div className="user-list">
<input
type="search"
placeholder="사용자 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<ul>
{filteredUsers.map(user => (
<UserItem
key={user.id}
user={user}
onDelete={handleDelete}
/>
))}
</ul>
{filteredUsers.length === 0 && (
<p>검색 결과가 없습니다.</p>
)}
</div>
);
}
// 자식 컴포넌트 - React.memo로 불필요한 리렌더링 방지
const UserItem = React.memo(function UserItem({
user,
onDelete
}: {
user: User;
onDelete: (id: number) => void;
}) {
return (
<li className="user-item">
<span>{user.name}</span>
<span>{user.email}</span>
<button onClick={() => onDelete(user.id)}>삭제</button>
</li>
);
});
// 재사용 가능한 Custom Hook
import { useState, useEffect, useRef } from 'react';
// 데이터 페칭 Hook
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// 로컬 스토리지 동기화 Hook
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue] as const;
}
// 디바운스 Hook
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 사용 예시
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const [history, setHistory] = useLocalStorage<string[]>('search-history', []);
const { data, loading } = useFetch<SearchResult[]>(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="검색어 입력"
/>
{loading ? <p>검색 중...</p> : (
<ul>
{data?.map(item => <li key={item.id}>{item.title}</li>)}
</ul>
)}
</div>
);
}