🌐 웹개발

React

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 기본
// 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 패턴
// 재사용 가능한 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>
    );
}

🗣️ 실무 대화 예시

💼 프로젝트 킥오프
"새 프로젝트 프론트엔드 스택을 React로 할까요, Vue로 할까요?"

"팀원 대부분이 React 경험이 있고 생태계도 더 크니까 React가 좋겠어요. Next.js 쓰면 SSR, API Routes까지 한 번에 되고, TypeScript랑 조합도 잘 돼요. 상태 관리는 TanStack Query + Zustand 조합이 요즘 트렌드입니다."
🔍 코드 리뷰
"이 컴포넌트가 왜 계속 리렌더링되는 거죠? Props가 안 바뀌는데..."

"부모 컴포넌트에서 inline 함수랑 객체를 props로 넘기고 있어서요. 매 렌더마다 새 참조가 생겨서 React.memo가 동작 안 해요. useCallback이랑 useMemo로 감싸거나, 객체는 컴포넌트 바깥에 정의해야 해요."
📱 기술 면접
"Virtual DOM이 왜 빠른가요? 실제 DOM을 직접 조작하는 것보다 느리지 않나요?"

"Virtual DOM 자체가 빠른 게 아니라, 변경 감지와 최소화가 빠른 거예요. 개발자가 직접 최적화된 DOM 조작을 할 수 있다면 더 빠를 수 있지만, 실수하기 쉽고 유지보수가 어려워요. React는 Diffing 알고리즘으로 필요한 변경만 일괄 처리해서 일관된 성능을 보장합니다."

⚠️ 주의사항

  • useEffect 의존성 배열 - 의존성 배열을 비워두면 마운트 시 한 번만 실행되지만, 내부에서 사용하는 state나 props가 변경되어도 반영 안 됩니다. ESLint exhaustive-deps 규칙을 무시하지 마세요.
  • key prop 중요성 - 리스트 렌더링 시 key에 index를 쓰면 순서 변경, 삽입, 삭제 시 예상치 못한 버그가 발생합니다. 항상 고유한 id를 사용하세요.
  • 과도한 상태 끌어올리기 - 모든 상태를 최상위로 올리면 불필요한 리렌더링이 발생합니다. 상태는 필요한 가장 가까운 공통 조상에만 두세요.

🔗 관련 용어

📚 더 배우기