Post

React를 위한 Modern JavaScript 핵심 정리

함수 일급, 불변성, spread/destructuring, map/filter를 React 관점으로 정리

React를 위한 Modern JavaScript 핵심 정리

React 학습/실무에서 “JS 문법” 자체보다 중요한 건 JS의 실행 모델과 데이터 흐름을 React 방식으로 다루는 능력이다. React는 UI를 “함수로 계산”하고, 변경은 “불변성 기반으로 추적”한다. 그래서 특정 JS 패턴이 사실상 필수 전제가 된다.


함수가 값이라는 개념과 React에서의 의미

개념

JavaScript에서 함수는 1급 객체(First-class citizen)다. 즉,

  • 변수에 담기고
  • 인자로 전달되고
  • 반환될 수 있다

React는 이 특성을 전제로 설계되어 있다.

왜 중요한가

React 컴포넌트 자체가 “함수”이고, 이벤트 핸들러/콜백/훅 의존성은 모두 “함수 값”을 다룬다.

  • 컴포넌트 = 함수: props -> UI(JSX)를 반환
  • 이벤트 핸들러 = 함수 전달: onClick={handleClick}
  • 상태 업데이트 = 함수 전달 가능: setState(prev => next)
  • 최적화(useCallback) = 함수의 동일성 유지

즉 React는 “함수를 실행한다”가 아니라 함수를 데이터처럼 조합해 UI를 계산하는 시스템이다.

언제 문제가 되는가 (대표 버그)

1) 함수를 “호출”해서 넘겨버리는 실수

1
<button onClick={handleClick()}>클릭</button>

이 코드는 클릭 시 실행이 아니라 렌더링 시 즉시 실행된다. React는 onClick에 “함수 값”을 기대한다.

정답:

1
<button onClick={handleClick}>클릭</button>

2) 렌더마다 새 함수 생성 → 불필요한 렌더/이펙트 트리거

1
<MyChild onSave={() => save(data)} />

이 패턴 자체가 항상 나쁜 건 아니지만, 자식이 memo로 최적화되어 있거나, useEffect 의존성에 함수가 들어가면 문제가 된다. 렌더마다 함수 “값(참조)”이 달라지기 때문.


불변성(Immutability)이 왜 중요한지

개념

불변성이란 “원본을 직접 바꾸지 않고 새 값을 만들어 교체하는 방식”이다.

왜 중요한가

React는 상태 변경을 “감지”할 때 보통 참조(주소) 변화를 기준으로 판단한다. 즉, 값이 바뀌었는지 판단하려면 새 객체/새 배열이 만들어져야 한다.

  • setState(obj)에서 obj가 이전과 같은 참조면, 변경을 놓치거나 최적화가 깨진다.
  • 리스트 렌더링, memo 최적화, reducer 패턴 전부 불변성 전제다.

언제 문제가 되는가 (대표 버그)

1) state를 직접 mutate 해서 UI가 안 바뀌는 현상

1
2
3
const [items, setItems] = useState([]);
items.push(newItem);
setItems(items);

겉보기엔 업데이트 같지만, items는 같은 배열 참조다. React는 변경을 “새 값”으로 인식하기 어렵다.

정답:

1
setItems(prev => [...prev, newItem]);

2) 깊은 구조에서 부분 mutate → “가끔”만 재렌더되는 지옥

1
2
state.user.profile.name = "kim";
setState(state);

이건 나중에 최적화(memo, selector) 들어가면 바로 폭발한다. 문제는 “지금 당장 동작하는 것처럼 보일 수 있다”는 점이다.


spread / destructuring이 state 업데이트에서 쓰이는 이유

개념

  • spread: 기존 값 복사 + 일부 변경(얕은 복사)
  • destructuring: 필요한 값만 꺼내서 더 명확하게 다룸

왜 중요한가

React 상태 업데이트는 본질적으로 “기존 값 기반으로 새 값 생성”이다. 이때 가장 간단한 도구가 spread다.

객체 업데이트

1
2
3
4
setUser(prev => ({
  ...prev,
  name: "kim",
}));

배열 업데이트

1
setTodos(prev => prev.filter(t => t.id !== id));

또한 destructuring은 컴포넌트에서 props/state를 다룰 때 의도를 명확히 해준다.

1
2
3
4
function Profile({ user }) {
  const { name, email } = user;
  ...
}

언제 문제가 되는가 (대표 버그)

1) spread는 “깊은 복사”가 아니다

1
2
const next = { ...prev };
next.profile.name = "kim"; // profile은 같은 참조일 수 있음

profile 같은 중첩 객체는 여전히 같은 참조라서 깊은 구조 mutate가 발생한다.

중첩 업데이트는 단계별로 새 참조를 만들어야 한다.

1
2
3
4
5
6
7
setUser(prev => ({
  ...prev,
  profile: {
    ...prev.profile,
    name: "kim",
  },
}));

map / filter가 JSX 렌더링과 연결되는 방식

개념

React 렌더링은 본질적으로 “데이터 → UI 리스트” 변환이다. 그 변환이 JS에서 가장 자연스럽게 표현되는 도구가 map, filter다.

왜 중요한가

  • map: 데이터의 각 원소를 컴포넌트(또는 JSX)로 변환
  • filter: 조건에 맞는 데이터만 남겨 렌더링 대상을 결정

예시:

1
2
3
4
5
6
7
<ul>
  {todos
    .filter(t => !t.done)
    .map(t => (
      <li key={t.id}>{t.title}</li>
    ))}
</ul>

이건 단순 반복문이 아니라 “UI를 데이터로부터 계산”하는 선언적 모델이다.

언제 문제가 되는가 (대표 버그)

1) filter/map에서 원본 배열을 건드리는 습관

sort()는 원본을 mutate한다. 렌더 중 sort()를 호출하면 상태/props를 오염시켜 예측 불가가 된다.

1
items.sort(...).map(...)

정답:

1
[...items].sort(...).map(...)

2) key를 잘못 줘서 상태가 섞임

map 렌더링에서 key={index}항목 삽입/삭제/정렬이 발생하면 컴포넌트 정체성이 깨진다. 그 결과 입력값이 다른 행으로 이동하거나, 체크박스 상태가 섞이는 버그가 나온다.


결론: React가 요구하는 JS는 “문법”이 아니라 “모델”이다

React는 UI를 함수로 계산하고, 변경을 참조 변화로 추적한다. 그래서 다음이 필수 전제가 된다.

  • 함수는 값이다 (컴포넌트/콜백/의존성)
  • 불변성은 생존 조건이다 (참조 변화로 업데이트 감지)
  • spread/destructuring은 “새 상태 생성”의 기본 도구다
  • map/filter는 “데이터→UI 변환”의 핵심 표현이다

This post is licensed under CC BY 4.0 by the author.