DEV/React

[React 개발 기초] 8. 참조형 State

develop-ch94 2026. 8. 10. 19:05
학습 목표: 객체와 배열을 State로 쓸 때의 함정을 피하고, 불변성을 지키며 안전하게 업데이트함

 

(1) "참조형"이란?

구분 예시 특징
원시형 숫자, 문자열, 불린 값 자체를 비교
참조형 객체, 배열 메모리 주소를 비교

 

// 원시형 — 값이 같으면 같은 것
5 === 5                // true
'hi' === 'hi'          // true

// 참조형 — 모양이 같아도 '다른 것'!
{ a: 1 } === { a: 1 }  // false 😱
[1, 2] === [1, 2]      // false 😱
왜 false? 참조형은 "같은 메모리 주소를 가리키는가"로 비교함
모양은 똑같아도 새로 만든 객체는 다른 주소라 false임

 

React랑 무슨 상관?

React는 State가 변했는지 확인할 때 ===(참조) 비교를 함

const obj = { name: '홍길동' };
obj.name = '김철수';   // 값만 바꿈
setUser(obj);          // 같은 주소의 객체

// React: "어? 주소가 똑같네? 안 바뀐 거네!" 😴
// → 화면 업데이트 안 함!

(2) 불변성(Immutability) - 오늘의 절대 원칙

불변성 = 기존 값을 수정하지 않고, 항상 새로 만들기

// ❌ 기존 객체 수정(React가 못 알아챔)
user.name = '김철수';
setUser(user);

// ✅ 새 객체 생성(React가 알아챔)
setUser({ ...user, name: '김철수' });

(3) 객체 State 다루기

기본 패턴: { ...기존객체, 바꿀속성: 새값 }

function UserProfile() {
  const [user, setUser] = useState({
    name: '홍길동',
    age: 25,
    email: 'hong@example.com'
  });
  
  function updateName(newName) {
    setUser({
      ...user,		// 기존 속성 모두 복사
      name: newName	// name만 덮어쓰기
    });
  }
  
  // ...
}

 

...user의 역할

setUser({ ...user, name: '김철수' });
//        ↑↑↑↑↑↑
//    "기존 속성을 전부 펼쳐서 복사해줘"

 

펼친 다음에 name: '김철수' 가 있으면 덮어쓰기가 됨

// 이렇게 풀어쓰면:
{
  name: '홍길동',    // ...user가 펼친 것
  age: 25,
  email: '...',
  name: '김철수'     // ← 덮어써짐!
}
// 결과: name만 바뀌고 나머진 유지! ✨

 

문제 해결

// ❌ 이러면 name, email이 사라짐
setUser({ age: 26 });

// ✅ 이렇게 해야 나머진 보존!
setUser({ ...user, age: 26 });
한 줄 요약: "기존 것 다 펼치고, 바꿀 것만 덮어쓰기"

(4) 배열 State 다루기 - 3대 패턴

공통 준비

const [todos, setTodos] = useState([
  { id: 1, text: '운동하기', done: false },
  { id: 2, text: '책 읽기', done: false }
]);

 

➕ 패턴 1: 추가 — Spread [...arr, 새항목]

function addTodo(newTodo) {
  setTodos([...todos, newTodo]); // 새 배열
}

 

❌ 절대 하지 마세요

todos.push(newTodo);	// 원본 수정! React가 모름
setTodos(todos);

 

➖ 패턴 2: 삭제 — filter(조건)

function deleteTodo(id) {
  setTodos(todos.filter(todo => todo.id !== id));
  //              ↑↑↑↑↑↑
  //     "id가 일치하지 않는 것만 남겨줘"
}
filter는 조건에 맞는 항목들로 새 배열을 만듦. 원본을 수정하지 않아서 완벽함

 

✏️ 패턴 3: 수정 — map() + 조건

function toggleTodo(id) {
  setTodos(todos.map(todo =>
    todo.id === id
      ? { ...todo, done: !todo.done } // 해당 항목만 바꾸기
      : todo						  // 나머진 그대로
  ));
}
해석: "각 todo를 살펴보면서, id가 일치하면 done을 뒤집고, 아니면 그대로 두세요."

 

공통점: 셋 다 새 배열을 만들어 교체함. 원본은 절대 건드리지 않음

 

완성 예제: Todo 리스트

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '운동하기', done: false },
    { id: 2, text: '책 읽기', done: false }
  ]);
  
  function toggleTodo(id) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo 
    ));
  }
  
  function deleteTodo(id) {
    setTodos(todos.filter(todo => todo.id !== id));
  }
  
  return (
    <div>
      {todos.map(todo => (
        <div
          key={todo.id}	// index 말고 반드시 고유 id
          style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
        >
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => toggleTodo(todo.id)}
          />
          {todo.text}
          <button onClick={() => deleteTodo(todo.id)}>삭제</button>
        </div>
      ))}
    </div>
  );
}

(5) 중첩된 객체 - 피해야 할 함정

객체 안에 객체가 있는 구조는 스프레드 지옥을 만듦

 

중첩이 깊어질수록 끔찍

const [settings, setSettings] = useState({
  user: {
    name: '홍길동',
    preferences: {
      theme: 'dark',
      language: 'ko'
    }
  }
});

// theme만 바꾸려면?
setSettings{
  ...settings,
  user: {
    ...settings.user,
    preferences: {
      ...settings.user.preferences,
      theme: 'light'
    }
  }
});

 

✅ 해결책 1: 구조를 평평하게

// 이렇게 나눠서 관리
const [userName, setUserName] = useState('홍길동');
const [theme, setTheme] = useState('dark');
const [language, setLanguage] = useState('ko');

// theme만 바꾸기
setTheme('light');

 

✅ 해결책 2: 복잡하면 Immer 같은 라이브러

setSettings(draft => {
  draft.user.preferences.theme = 'light'; // 직접 수정하듯이 작성 가능
});

(6) 자주 하는 안티패턴

❌ 안티패턴 1: 배열 메서드 중 원본을 수정하는 것 사용

// 이 메서드들은 원본을 수정해요! 🚨
todos.push(newTodo);       // 추가 - 원본 수정!
todos.pop();               // 삭제 - 원본 수정!
todos.splice(0, 1);        // 삭제 - 원본 수정!
todos.sort();              // 정렬 - 원본 수정!
todos.reverse();           // 뒤집기 - 원본 수정!
대신 사용할 것:
 - push → [...todos, new]
 - sort → [...todos].sort() (복사본 정렬)
 - revers → [...todos].reverse() (복사본 뒤집기)

 

❌ 안티패턴 2: 불완전한 객체 업데이트

// ❌ 나머지 속성이 전부 사라짐!
setUser({ age: 26 });

// ✅ spread로 기존 속성 보존
setUser({ ...user, age: 26 });

 

❌ 안티패턴 3: 객체/배열 직접 수정

// ❌ React가 변화 감지 못함!
user.name = '김철수';
setUser(user);

todos[0].done = true;
setTodos(todos);
이유: setter에 같은 주소의 객체/배열을 넘기면, React는 "안 바뀐 거네" 판단함

 

❌ 안티패턴 4: 중첩 객체로 들어가기 쉬움

// ❌ 너무 깊은 중첩
const [data, setData] = useState({
  user: {
    info: {
      personal: {
        name: '홍길동'   // 여기 접근하려면 data.user.info.personal.name...
      }
    }
  }
});

// ✅ 평평하게 재설계
const [userName, setUserName] = useState('홍길동');
// 또는 관심사별로 분리된 여러 useState