React 개발 치트시트 & 가이드 (심화)
이 문서는 React 개발의 핵심 개념과 모범 사례를 정리한 치트시트 및 가이드입니다.
1. Props vs State
Props와 State는 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: 전역적으로 사용될 데이터를 (
propsdrilling 없이) 공유합니다. - 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 (업데이트):
props나state가 변경되어 리렌더링될 때useEffect(() => { ... }, [dependency1, dependency2])- ※ 중요: 이
useEffect는 컴포넌트가 처음 마운트될 때도 1회 실행되고, 그 이후에는 의존성 배열(deps) 안의 값이 변경될 때마다 실행됩니다.
- Unmount (언마운트): 컴포넌트가 DOM에서 제거될 때
useEffect(() => { return () => { /* cleanup logic */ } }, [])
8. 커스텀 훅 (Custom Hooks)
커스텀 훅은 컴포넌트의 반복적인 상태 관련 로직을 함수로 추출하여 재사용할 수 있게 만드는 기능입니다.
커스텀 훅의 규칙
- 이름이 반드시
use로 시작해야 합니다. (예:useFetch,useToggle) - 최상위 레벨에서만 호출할 수 있습니다. (반복문, 조건문, 중첩 함수 내에서 호출 불가)
- 오직 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>
);
};
댓글 (0)