React 개발 치트시트 & 가이드 (심화)

이 문서는 React 개발의 핵심 개념과 모범 사례를 정리한 치트시트 및 가이드입니다.


1. Props vs State

PropsState는 React에서 데이터를 다루는 두 가지 핵심 개념이지만, 역할과 동작 방식에 명확한 차이가 있습니다.

구분Props (Properties)State
소유권부모 컴포넌트컴포넌트 자신
수정 가능 여부자식에서 직접 수정 불가 (Immutable)컴포넌트 내에서 setState 등으로 수정 가능
데이터 흐름단방향 (부모 → 자식)컴포넌트 내부에서 시작되는 단방향 흐름
목적컴포넌트의 설정 값, 데이터 전달컴포넌트의 동적인 상태(UI 변화 등) 관리
비유함수의 매개변수 (arguments)함수 내에 선언된 지역 변수 (local variables)

핵심: props는 밖에서 주입되는 값이고, state는 안에서 관리되는 값입니다.

예시: 부모의 Props를 전부 넘기는 자식 컴포넌트

스프레드(...) 연산자를 사용하면 부모 컴포넌트가 가진 여러 props를 자식 컴포넌트에 한 번에 전달할 수 있습니다.

// ChildComponent는 부모가 전달하는 모든 props를 받습니다.
const ChildComponent = (props) => {
  // props 객체 전체를 받아서 필요한 값을 사용합니다.
  // 이 컴포넌트는 name과 age 외에 city prop도 받았지만 사용하지는 않습니다.
  return (
    <div>
      <p>이름: {props.name}</p>
      <p>나이: {props.age}</p>
    </div>
  );
};
 
// ParentComponent는 ChildComponent에 props를 전달합니다.
const ParentComponent = () => {
  const userProps = {
    name: '홍길동',
    age: 25,
    city: '서울'
  };
 
  // 스프레드(...) 연산자를 사용하여 userProps 객체의 모든 속성을
  // ChildComponent의 props로 한 번에 전달합니다.
  // <ChildComponent name="홍길동" age={25} city="서울" /> 와 동일합니다.
  return (
    <div>
      <h1>부모 컴포넌트</h1>
      <ChildComponent {...userProps} />
    </div>
  );
};

2. 데이터 흐름 (단방향 데이터 흐름)

React는 부모에서 자식으로 흐르는 단방향 데이터 흐름을 따릅니다.

상태(State) 변경 발생
  ↓
① 컴포넌트 리렌더링 (Re-rendering)
  ↓
② 자식 컴포넌트에 Props 전달
  ↓
③ 자식 컴포넌트 리렌더링
  ↓
④ DOM 업데이트

3. 추천 폴더 구조

src/
├── api/                     # API 호출 함수 (axios, fetch)
├── assets/                  # 이미지, 폰트 등 정적 파일
├── components/              # 재사용 가능한 공통 컴포넌트
├── contexts/                # Context API 관련 파일
├── hooks/                   # 커스텀 훅
├── pages/ (or views/)       # 라우팅 단위의 페이지 컴포넌트
├── store/ (or redux/)       # 전역 상태 관리 (Redux, Zustand 등)
├── styles/                  # 전역 스타일, 테마
├── types/                   # 공통 타입 정의 (interfaces)
├── utils/                   # 유틸리티 함수
├── App.tsx
└── index.tsx

4. 주요 Hooks 치트시트

주요 Hooks 심화: 언제 무엇을 쓸까?

useState vs useReducer

  • useState: 간단한 상태(숫자, 문자열, boolean 등)나 독립적인 상태 관리에 적합합니다.
  • useReducer: 여러 하위 값을 포함하는 복잡한 상태 객체, 다음 state가 이전 state에 의존적인 경우, 또는 상태 관리 로직을 컴포넌트 밖으로 분리하고 싶을 때 유용합니다. 테스트 용이성이 높아집니다.

useCallback vs useMemo

  • useCallback: 함수 자체를 메모이제이션합니다. 자식 컴포넌트에 props로 함수를 전달할 때, 부모 컴포넌트가 리렌더링되어도 함수가 재성성되지 않도록 하여 자식의 불필요한 리렌더링을 방지합니다.
  • useMemo: 함수의 반환 값을 메모이제이션합니다. 렌더링 중에 수행되는 비용이 큰 연산의 결과를 저장하여, 의존성이 변경되지 않으면 재연산 없이 저장된 값을 재사용합니다.
  • 사실 useCallback(fn, deps)useMemo(() => fn, deps)와 같습니다.

5. 컴포넌트 렌더링 최적화

  • React.memo(Component): 컴포넌트를 감싸서 props가 변경되지 않으면 리렌더링을 방지합니다. (HOC)
  • useCallback: 자식 컴포넌트에 props로 전달하는 함수를 메모이제이션합니다.
  • useMemo: 복잡한 연산 결과를 메모이제이션합니다.

렌더링 최적화 종합 예제

import React, { useState, useCallback, useMemo } from 'react';
 
const OptimizedChild = React.memo(({ user, onUpdate }) => {
  console.log('자식 컴포넌트가 렌더링되었습니다.');
  return (
    <div>
      <p>사용자 이름: {user.name}</p>
      <button onClick={onUpdate}>사용자 업데이트</button>
    </div>
  );
});
 
const ParentComponent = () => {
  const [unrelatedState, setUnrelatedState] = useState(0);
  const [user, setUser] = useState({ name: '철수', age: 30 });
 
  const memoizedUser = useMemo(() => ({ name: user.name }), [user.name]);
  const handleUpdateUser = useCallback(() => {
    setUser(prev => ({ ...prev, name: '영희' }));
  }, []);
 
  console.log('부모 컴포넌트가 렌더링되었습니다.');
 
  return (
    <div>
      <button onClick={() => setUnrelatedState(c => c + 1)}>
        상관없는 상태 변경: {unrelatedState}
      </button>
      <OptimizedChild user={memoizedUser} onUpdate={handleUpdateUser} />
    </div>
  );
};

6. 상태 관리(State Management) 패턴

  • Local State: useState, useReducer를 사용하여 단일 컴포넌트 또는 가까운 자식과 상태를 공유합니다.
  • State Lifting (상태 끌어올리기): 여러 자식 컴포넌트가 공유해야 하는 상태를 가장 가까운 공통 부모로 이동시킵니다.
  • Context API: 전역적으로 사용될 데이터를 (props drilling 없이) 공유합니다.
  • External Libraries: 복잡하고 거대한 애플리케이션의 전역 상태 관리를 위해 사용합니다. (Redux, Zustand 등)

상태 끌어올리기(State Lifting) 예제

import React, { useState } from 'react';
 
const SharedInput = ({ label, value, onChange }) => {
  return (
    <div>
      <label>{label}: </label>
      <input type="text" value={value} onChange={e => onChange(e.target.value)} />
    </div>
  );
};
 
const StateLiftingParent = () => {
  const [text, setText] = useState('');
 
  return (
    <div>
      <h3>입력 값을 공유하는 두 컴포넌트</h3>
      <SharedInput label="입력 A" value={text} onChange={setText} />
      <SharedInput label="입력 B" value={text} onChange={setText} />
      <p>현재 공유된 : {text}</p>
    </div>
  );
};

7. 컴포넌트 라이프사이클 (Hooks 기준)

  • Mount (마운트): 컴포넌트가 처음 DOM에 렌더링될 때
    • useEffect(() => { ... }, [])
  • Update (업데이트): propsstate가 변경되어 리렌더링될 때
    • useEffect(() => { ... }, [dependency1, dependency2])
    • ※ 중요: 이 useEffect컴포넌트가 처음 마운트될 때도 1회 실행되고, 그 이후에는 의존성 배열(deps) 안의 값이 변경될 때마다 실행됩니다.
  • Unmount (언마운트): 컴포넌트가 DOM에서 제거될 때
    • useEffect(() => { return () => { /* cleanup logic */ } }, [])

8. 커스텀 훅 (Custom Hooks)

커스텀 훅은 컴포넌트의 반복적인 상태 관련 로직을 함수로 추출하여 재사용할 수 있게 만드는 기능입니다.

커스텀 훅의 규칙

  1. 이름이 반드시 use로 시작해야 합니다. (예: useFetch, useToggle)
  2. 최상위 레벨에서만 호출할 수 있습니다. (반복문, 조건문, 중첩 함수 내에서 호출 불가)
  3. 오직 React 함수 컴포넌트 또는 다른 커스텀 훅 내에서만 호출할 수 있습니다.

커스텀 훅 예제 (useToggle)

Boolean 값을 toggle하는 간단한 useToggle 훅 예제입니다.

import { useState, useCallback } from 'react';
 
// useToggle 커스텀 훅 정의
export const useToggle = (initialState: boolean = false): [boolean, () => void] => {
  const [state, setState] = useState<boolean>(initialState);
 
  const toggle = useCallback(() => {
    setState(prevState => !prevState);
  }, []);
 
  return [state, toggle];
};
 
// 커스텀 훅 사용 예제
const ToggleComponent = () => {
  const [isToggled, toggle] = useToggle(false);
 
  return (
    <div>
      <button onClick={toggle}>
        {isToggled ? 'ON' : 'OFF'}
      </button>
      {isToggled && <p>보여지는 내용</p>}
    </div>
  );
};

9. 에러 바운더리 (Error Boundaries)

에러 바운더리는 하위 컴포넌트 트리에서 발생하는 자바스크립트 에러를 포착하여, 앱 전체가 중단되는 대신 폴백(fallback) UI를 보여줄 수 있게 하는 컴포넌트입니다.

에러 바운더리의 특징

  • 클래스 컴포넌트여야 합니다. 현재까지 Hooks API로는 구현할 수 없습니다.
  • getDerivedStateFromError 또는 componentDidCatch 생명주기 메서드 중 하나 이상을 정의해야 합니다.
  • 자신의 에러는 잡을 수 없고, 오직 자식 컴포넌트 트리의 에러만 잡을 수 있습니다.
  • 비동기 코드(e.g., setTimeout), 이벤트 핸들러, 서버 사이드 렌더링에서는 동작하지 않습니다.

에러 바운더리 구현 예제

import React, { Component, ErrorInfo, ReactNode } from 'react';
 
interface Props {
  children: ReactNode;
}
 
interface State {
  hasError: boolean;
}
 
class ErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false,
  };
 
  public static getDerivedStateFromError(_: Error): State {
    return { hasError: true };
  }
 
  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Uncaught error:', error, errorInfo);
    // 외부 로깅 서비스에 에러를 리포트할 수 있습니다. (e.g., Sentry)
  }
 
  public render() {
    if (this.state.hasError) {
      return <h1>문제가 발생했습니다. 잠시 후 다시 시도해주세요.</h1>;
    }
 
    return this.props.children;
  }
}
 
export default ErrorBoundary;

에러 바운더리 사용법

에러가 발생할 가능성이 있는 컴포넌트를 ErrorBoundary로 감싸줍니다.

import ErrorBoundary from './ErrorBoundary';
import ProblematicComponent from './ProblematicComponent';
 
const App = () => {
  return (
    <div>
      <h1>My Application</h1>
      <ErrorBoundary>
        <ProblematicComponent />
      </ErrorBoundary>
    </div>
  );
};