React Deep Dive Study - 2

_sw_·2025년 11월 30일

!!. useState와 useReducer

  1. update 방식을 제한할 수 있다.
// ❌ useState: 어떤 방식으로든 수정 가능
const [state, setState] = useState({ count: 0, user: {} });
setState({ count: 999 });  // 가능
setState({});  // 이것도 가능... 😱

// ✅ useReducer: 정의된 action으로만 수정 가능
const [state, dispatch] = useReducer(reducer, initialState);

dispatch({ type: 'INCREMENT' });  // ✅ 허용된 action
dispatch({ type: 'UPDATE_USER', payload: user });  // ✅ 허용된 action
dispatch({ type: 'RANDOM_ACTION' });  // ❌ reducer에서 처리 안 됨
  1. 비슷한 성격의 state를 묶어서 관리할 수 있다.

+) state를 조작하는 Action도 묶어서 관리할 수 있다. reducer 내부에서 관리되기 때문에 응집도가 높다.

// ❌ useState: state가 분산됨
function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState(0);
  const [address, setAddress] = useState('');
  const [phoneNumber, setPhoneNumber] = useState('');
  // state가 5개로 분산되어 관리가 어려움
}

// ✅ useReducer: 관련된 state를 하나로 묶음
function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return { ...state, [action.field]: action.value };
    case 'RESET_FORM':
      return initialFormState;
    default:
      return state;
  }
}

const initialFormState = {
  name: '',
  email: '',
  age: 0,
  address: '',
  phoneNumber: ''
};

function Form() {
  const [formData, dispatch] = useReducer(formReducer, initialFormState);
  
  // 하나의 state로 관련 데이터 관리
  // 모든 업데이트 로직이 reducer에 집중
  const updateField = (field, value) => 
    dispatch({ type: 'UPDATE_FIELD', field, value });
}

??. useEffect는 언제 써야할까?

useEffect는 '부수효과'를 처리하는 로직이다보니 설명이 사이드 이펙트처럼 느껴지기도 했고, 그만큼 디버깅도 어려운 훅이라고 생각이 들어 잘 사용해야하는 훅이라고 생각이 든다.

아래는 공식 문서에서 제공하는 useEffect 사용을 고민해야하는 사례를 설명하고 있는데 그중 몇가지만 소개해보려고 한다.
You Might Not Need an Effect – React

사례 1: Props 변경 시 State 초기화하기

function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 🔴 userId가 변경될 때마다 comment를 초기화하려고 Effect 사용
  useEffect(() => {
    setComment('');
  }, [userId]);

  return (
    <div>
      <h1>User {userId}'s Profile</h1>
      <textarea
        value={comment}
        onChange={e => setComment(e.target.value)}
      />
    </div>
  );
}

문제점:

  • Effect가 실행되면서 불필요한 리렌더링 발생
  • userId 변경 → 렌더링 → Effect 실행 → 다시 렌더링 (2번 렌더링!)

수정안

function ProfilePage({ userId }) {
  return <Profile userId={userId} key={userId} />;
}

function Profile({ userId }) {
  // ✅ key가 변경되면 React가 자동으로 컴포넌트를 재생성
  // State가 자동으로 초기화됨
  const [comment, setComment] = useState('');

  return (
    <div>
      <h1>User {userId}'s Profile</h1>
      <textarea
        value={comment}
        onChange={e => setComment(e.target.value)}
      />
    </div>
  );
}

장점:

  • Effect 없이 자연스럽게 state 초기화
  • 1번의 렌더링만 발생
  • React의 key 메커니즘 활용

사례 2: 렌더링을 위한 데이터 변환

function TodoList({ todos, filter }) {
  const [visibleTodos, setVisibleTodos] = useState([]);

  // 🔴 todos나 filter가 변경될 때마다 Effect로 필터링
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  return (
    <ul>
      {visibleTodos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

문제점:

  • 불필요한 state와 Effect
  • todos/filter 변경 → 렌더링 → Effect 실행 → 다시 렌더링 (2번 렌더링!)
  • 메모리 낭비 (state를 별도로 저장)

수정안

function TodoList({ todos, filter }) {
  // ✅ 렌더링 중에 직접 계산
  const visibleTodos = getFilteredTodos(todos, filter);

  return (
    <ul>
      {visibleTodos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

function getFilteredTodos(todos, filter) {
  if (filter === 'active') {
    return todos.filter(todo => !todo.completed);
  }
  if (filter === 'completed') {
    return todos.filter(todo => todo.completed);
  }
  return todos;
}

장점:

  • Effect 없이 간단하고 명확
  • 1번의 렌더링만 발생
  • 추가 state 불필요

오히려 이때는 useMemo 를 사용하는게 효과적

function TodoList({ todos, filter }) {
  // ✅ 비용이 큰 계산은 useMemo로 캐싱
  const visibleTodos = useMemo(() => {
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);

  return (
    <ul>
      {visibleTodos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

사례 3: 이벤트 핸들러에서 처리해야 할 로직

function ProductPage({ product, addToCart }) {
  // 🔴 product가 변경될 때마다 알림을 보내려고 Effect 사용
  useEffect(() => {
    if (product.isInCart) {
      showNotification(`${product.name}이(가) 장바구니에 추가되었습니다!`);
    }
  }, [product]);

  function handleBuyClick() {
    addToCart(product);
  }

  return (
    <button onClick={handleBuyClick}>
      구매하기
    </button>
  );
}

문제점:

  • 사용자가 구매 버튼을 누르지 않아도 알림이 뜰 수 있음
  • 페이지 새로고침 시에도 알림이 뜸
  • "특정 상호작용"에 대한 반응이 아니라 "상태 변화"에 반응
function ProductPage({ product, addToCart }) {
  function handleBuyClick() {
    // ✅ 사용자가 구매 버튼을 눌렀을 때만 실행
    addToCart(product);
    showNotification(`${product.name}이(가) 장바구니에 추가되었습니다!`);
  }

  return (
    <button onClick={handleBuyClick}>
      구매하기
    </button>
  );
}

장점:

  • 사용자의 특정 행동에 정확히 반응
  • 의도가 명확함
  • 예상치 못한 시점에 실행되지 않음

그래서, Effect는 언제 사용하나?

// ✅ 외부 시스템과 동기화할 때만 사용
useEffect(() => {
  // 브라우저 API
  const connection = createConnection(serverUrl, roomId);
  connection.connect();

  return () => {
    connection.disconnect();
  };
}, [serverUrl, roomId]);

외부 시스템의 예:

  • 네트워크 요청
  • 브라우저 API (localStorage, geolocation 등)
  • 타사 라이브러리 (D3, jQuery 등)
  • 타이머 (setTimeout, setInterval)

관련 ESlint Plugin

ESLint - React - You Might Not Need An Effect

??.useRef는 왜 current를 가지고 있을까? 무슨 의도를 갖고 있는 걸까?

useRef는 기본적으로 렌더링에 필요하지 않을 값을 관리하려는 의도를 가지고 있는 훅이다.

만약 useRef내부가 일반 변수들 처럼 primitive하게 관리되었다면 매번 호출에 있어 초기화될 것이다. (불변성 유지 X)

// ❌ primitive로 관리했다면?
function Component() {
  let count = 0;  // 매 렌더마다 0으로 초기화
  
  const increment = () => {
    count += 1;
    console.log(count);  // 1, 2, 3...
  };
  
  return <button onClick={increment}>Click</button>;
  // 리렌더링 시 count는 다시 0이 됨
}

반대로 useState 처럼 관리된다면 useRef의 의도와 멀어지게 된다. ( 랜더링 발생 )

// ❌ useState로 관리했다면?
function Component() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);  // 값은 유지되지만 리렌더링 발생!
  };
  
  return <button onClick={increment}>Click</button>;
}

위 두 문제 상황으로 접근해보면 useRef는 상태 변화에 있어서 리랜더링이 발생하면 안되고, 불변성이 유지되어야한다.

따라서 값은 바뀌어야하는데, 그 대상은 불변해야한다라는 모순적인 요구사항이 생기게 된다.

그래서 객체를 통해 이를 만족시키고자 한 것 같다.

// ✅ useRef의 실제 구조
const ref = useRef(0);

// ref 객체 자체는 불변 (같은 참조 유지)
console.log(ref);  // { current: 0 }

// current만 변경 가능
ref.current = 5;
ref.current = 10;

// 리렌더링 후에도
console.log(ref === 이전_렌더의_ref);  // true (같은 객체!)

객체를 선언한 대상으로 불변성을 유지하고, 내부의 값(current)를 통해서 값은 지속적으로 바뀔 수 있도록 의도했다.

뿐만 아니라 React 내부에서 선언된 상태는 ReactFiber를 통해 사용되는 컴포넌트로 전달된다. 이때는 JS가 값을 주고받는 원리와 연결되어 있는데

// JavaScript 값 전달 특성

// Primitive: 값 복사
let a = 5;
let b = a;  // 새로운 메모리에 5를 복사
a = 10;
console.log(b);  // 5 (변경 안됨)

// Object: 참조 전달
let obj1 = { value: 5 };
let obj2 = obj1;  // 같은 객체를 참조
obj1.value = 10;
console.log(obj2.value);  // 10 (같은 객체를 가리킴)

primvitive한 값인 경우에는 동일한 값을 복사하지만, 객체는 원본 객체를 참조하는 값을 전달하기 때문에 React Fiber 작업내에서도 불변성을 유지할 수 있는 점 때문에 객체를 채택한 것 같다.

current라는 필드 명은 네이밍 적인 요인으로 ‘현재 저장된 값’으로 생각하면 납득이 될 것 같다.

profile
나도 잘하고 싶다..!

0개의 댓글