JAN's History

React 학습하기 - 상호작용 더하기 본문

React

React 학습하기 - 상호작용 더하기

JANNNNNN 2025. 7. 30. 22:11

https://ko.react.dev/learn/responding-to-events 를 보고 정리한 블로그입니다.

1. 이벤트에 응답하기 (Responding to Events)


React에서는 브라우저 DOM 요소에 이벤트 핸들러(event handler) 를 연결하여 사용자 입력에 반응할 수 있다

JSX에서 이벤트 핸들러 추가하기

  • HTML처럼 onclick, onchange가 아니라 카멜케이스 onClick, onChange 사용
  • 값으로는 문자열이 아니라 함수 자체를 전달해야 함
<button onClick={handleClick}>Click me</button>

이벤트 핸들러 함수 만들기

  • 컴포넌트 안에 함수 정의 후 이벤트에 연결한다.합
  • 일반적으로 handleSomething 형식의 네이밍을 사용
function MyButton() {
  function handleClick() {
    alert('You clicked me!');
  }
 
  return <button onClick={handleClick}>Click me</button>;
}

이벤트 객체 사용하기

  • 이벤트 핸들러에 전달되는 첫 번째 인자는 이벤트 객체 (SyntheticEvent)
  • 예: onClick에서는 MouseEvent, onChange에서는 ChangeEvent 
function handleClick(e) {
  console.log(e); // SyntheticEvent
}

컴포넌트가 상태를 바꾸게 하기

  • 버튼 클릭 등의 이벤트를 통해 state 업데이트 가능
function MyButton() {
  const [count, setCount] = useState(0);
 
  function handleClick() {
    setCount(count + 1);
  }
 
  return <button onClick={handleClick}>Clicked {count} times</button>;
}

이벤트 핸들러에서 다른 함수 호출하기

  • 핸들러 안에서 다른 함수를 직접 호출하거나
  • 함수를 넘겨줄 수 있음
function handleClick(name) {
  alert(`Hello, ${name}`);
}
<button onClick={() => handleClick('Jaeeun')}>Greet</button>

주의할 점

  • JSX에서 ()를 붙이면 함수가 즉시 실행됨 → 핸들러로는 함수 참조만 전달

2. State: 컴포넌트의 기억 저장소

State는 컴포넌트의 기억 저장소이다. 컴포넌트가 사용자와의 상호작용 결과로 데이터를 저장하고, 그에 따라 화면을 다시 그리기 위해 사용된다.

React는 이러한 상태 정보를 컴포넌트 별로 관리할 수 있도록 useState 훅을 제공한다.

일반 변수의 한계

  • 다음 코드는 버튼 클릭 시 index를 증가시키고자 하지만 동작하지 않는다
let index = 0;
function handleClick() {
  index = index + 1;
}

왜 안 될까?

  1. 지역 변수는 렌더링 간 유지되지 않음
  2. React는 지역 변수 변화로 다시 렌더링하지 않음

따라서, UI가 반응형으로 업데이트되길 원한다면 반드시 state를 사용해야한다.

useState 훅으로 state 사용하기

  • 현재 값을 저장하는 state 변수 (index) 
  • state를 업데이트하고 렌더링을 유발하는 setter 함수 (setIndex)
import { useState } from 'react';
const [index, setIndex] = useState(0);  // 0은 초기값
 
function handleClick() {
  setIndex(index + 1);  // 새로운 값으로 갱신
}
  1. 초기 렌더링: [0, setIndex] 반환
  2. setIndex(1) 실행 시 → React가 다시 렌더링
  3. 이후 렌더링에선 [1, setIndex] 반환

⇒ state는 렌더링 간에도 값을 기억하고 UI를 일관되게 유지하는 핵심 요소이다

구조 분해 할당 문법

[value, setValue]는 배열 구조 분해 문법으로, 항상 두 개의 값이 들어있다.

  1. 현재 상태 값
  2. 상태 갱신 함수
const [value, setValue] = useState(initialValue);

여러 개의 state 변수 다루기

  • index: 현재 표시 중인 조각상 인덱스를 저장
  • showMore: 설명을 토글할지 여부를 제어하는 boolean
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);

각 state는 독립적으로 관리되므로, 관련성이 없을 경우 각각 나눠서 사용하는 것이 가독성과 유지보수에 좋다.

그러나 만약 여러 필드(state)를 한 번에 업데이트해야 하는 경우에는 객체 형태로 하나의 state로 묶는 것이 효율적이다.

상태 격리와 독립성

  • React의 state는 컴포넌트 인스턴스에 국한된 저장소이다.
  • 동일한 컴포넌트를 두 번 렌더링하면 각각의 복사본은 독립적인 state를 가진다.
  • 부모 컴포넌트는 자식의 state를 직접 알거나 수정할 수 없다.
  • 이는 state가 외부로부터 격리되고 캡슐화된다는 의미
<Gallery />
<Gallery />

=> 즉 위처럼 <Gallery />를 두 번 렌더링해도 각 갤러리는 독립적인 state를 유지한다. 하나의 갤러리에서 Next 버튼을 눌러도 다른 갤러리에는 영향이 없다.

3. 렌더링 그리고 커밋

React에서 컴포넌트를 화면에 표시하기 위한 전체 흐름은 다음 3단계로 이루어진다.

  1. 렌더링 트리거: 컴포넌트를 새로 렌더링해야 하는 이유가 발생
  2. 렌더링: React가 컴포넌트를 호출하여 JSX를 계산
  3. 커밋: 계산된 JSX를 실제 DOM에 반영

1. 렌더링 트리거

렌더링은 두 가지 경우에 발생한다.

  • 초기 렌더링 : 앱 시작 시 createRoot와 render()를 호출해 렌더링이 시작.
  • 상태 업데이트 useState set 수를 호출해 상태가 바뀌면, React는 해당 컴포넌트를 렌더링 대기열에 넣고 다시 렌더링을 시작한다.

2. React 컴포넌트 렌더링

React는 렌더링 트리거 후 컴포넌트를 호출하여 JSX를 계산한다. 렌더링 자체는 DOM을 변경하지 않으며, 순수한 계산 단계이다.

export default function Gallery() {
  return (
    <section>
      <h1>Inspiring Sculptures</h1>
      <Image />
      <Image />
      <Image />
    </section>
  );
}
function Image() {
  return (
    <img src="https://.." alt="..." />
  );
}

React는 Gallery  Image × 3 순서로 컴포넌트를 호출하며, 각 JSX 트리를 재귀적으로 계산

렌더링의 규칙 (순수성)

  • 같은 입력 → 같은 출력이어야 한다.
  • 기존 상태(state), 변수, 객체를 직접 수정하지 않아야 한다.
  • 이 규칙을 어기면 예측 불가능한 동작이 생길 수 있다.

개발 중에는 StrictMode를 통해 컴포넌트 렌더링을 2번 호출해서 이러한 실수를 잡아낼 수 있다.

3. DOM에 커밋

렌더링 단계가 끝나면 React는 변경된 부분을 DOM에 커밋(적용)

  • 초기 렌더링: appendChild() 등으로 DOM 노드를 생성
  • 리렌더링: 이전 결과와 비교해 필요한 최소 변경만 수행
export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}
  • time이 변경되면 <h1>만 새로 렌더링되고 <input>은 그대로 유지된다.
  • 그리고 time이 리렌더링 되어도 input에 텍스트가 사라지지 않는다

⇒ 마지막 변경 단계에서 React가 <input>이 JSX에서 동일한것이 확인되므로 React는 <input> 또는 value를 건드리지 않는다.

브라우저 페인트

렌더링이 완료되고 React가 DOM을 업데이트한 후 브라우저는 화면을 다시 그린다. 이 단계를 “브라우저 렌더링”이라고 하지만 이 문서의 나머지 부분에서 혼동을 피하고자 “페인팅”이라고 부른다.

4. 스냅샷으로서의 State

state 변경 흐름

  1. setState() 호출
  2. React가 다음 렌더링을 예약
  3. 컴포넌트를 다시 호출하여 JSX(스냅샷)를 반환
  4. React가 이 스냅샷에 맞게 실제 DOM을 업데이트

setNumber를 여러 번 호출해도 state는 1만 증가

<button onClick={() => {
  setNumber(number + 1);
  setNumber(number + 1);
  setNumber(number + 1);
}}>+3</button>
  • number가 0이면 → setNumber(0 + 1)이 3번 호출됨
  • 다음 렌더링에선 number가 1이 됨 (3이 아님)
  • 이유: number는 이벤트 핸들러 실행 시 "고정된 스냅샷"이기 때문

setTimeout 안의 state도 스냅샷으로 고정됨

setNumber(number + 5);
setTimeout(() => {
  alert(number); // 여전히 이전 렌더링의 number
}, 3000);
  • 3초 뒤에도 number는 여전히 "0"
  • 이유: setTimeout은 이벤트 핸들러 실행 당시의 스냅샷을 사용

이벤트 핸들러 안에서 state "고정" 확인

<form onSubmit={e => {
  e.preventDefault();
  setTimeout(() => {
    alert(`You said ${message} to ${to}`);
  }, 5000);
}}>
  • "To" 값을 Alice로 둔 채 Send를 클릭
  • 5초 이내에 To를 Bob으로 변경
  • Alert에는 여전히 "Alice" 가 출력됨
  • 이유: 핸들러가 생성될 당시의 state가 고정됨

요약

  • state는 일반 변수처럼 보이지만, 사실 스냅샷(snapshot)처럼 동작한다.
  • setState 호출 시, 현재 렌더링에는 영향을 주지 않고, 다음 렌더링을 큐(queue)에 넣는다.
  • React는 컴포넌트 외부에 state를 저장하고, 컴포넌트를 호출할 때 해당 시점의 state를 넘겨준다.
  • 이로 인해 이벤트 핸들러나 콜백 함수는 특정 시점의 state를 "고정"된 값으로 갖게 된다.

5. State 업데이트 큐

setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
  • 이 코드는 모든 setNumber(...)가 현재 렌더링 시점의 number 값(0)을 기준으로 동작함.
  • React는 이벤트 핸들러 실행이 끝난 후 batching(배치)해서 처리하므로,
    • setNumber(1)이 3번 큐에 들어가지만, 결과적으로는 같은 값으로 3번 대체하는 꼴이 됨.
  • 마지막 setNumber(1)만 반영됨

⇒ 최종결과 number = 1

setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
  • 이 코드는 각각의 setNumber 호출에 업데이터 함수를 사용하고 있음.
  • React는 이벤트가 끝난 뒤, 큐에 있는 함수를 순차적으로 실행해서 다음 상태를 계산함:
    1. n => n + 1  n = 0 → 1
    2. n => n + 1  n = 1 → 2
    3. n => n + 1  n = 2 → 3
  • 즉, 이전 상태를 기반으로 다음 상태를 계산하므로 업데이트가 누적됨.

⇒ 최종결과 number = 3

setNumber(number + 5);
setNumber(n => n + 1);
  • number + 5는 현재 상태(0)를 기준으로 계산된 정적 값이므로, setNumber(5)와 같음 → "5로 바꾸기"가 큐에 들어감.
  • 그 다음 setNumber(n => n + 1)은 5를 기반으로 1 증가시키는 함수로 동작함.
  • 결과적으로 React는 다음과 같이 처리함:
    1. 값 5로 교체
    2. 5를 받아서 +1 → 6

⇒ 최종결과 number = 6

setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
  • 큐에 다음과 같은 작업이 순서대로 들어감:
    1. setNumber(5) → 현재 상태인 0에 5 더해서 5로 설정
    2. setNumber(n => n + 1) → 그 직전 값 5를 받아 6으로 설정
    3. setNumber(42)  이전 모든 결과를 무시하고 42로 덮어씀
  • React는 마지막 값 설정(setNumber(42))이 들어오면 그 이전의 모든 계산 결과를 무시하고 덮어씀.

⇒ 최종결과 number = 42

요약

  • batching: React는 렌더링 성능을 높이기 위해 이벤트 핸들러 내에서 발생하는 여러 개의 setState 호출을 한 번의 렌더링으로 묶어 처리한다.
  • 업데이터 함수: setState(n => n + 1)처럼 이전 상태를 기반으로 다음 상태를 계산하는 함수를 전달할 수 있다. 이는 여러 번의 상태 변경을 순차적으로 적용할 수 있게 해준다.
  • 상태 설정 방식:
    • setState(5)는 “값을 대체”한다.
    • setState(n => n + 1)는 “값을 기반으로 연산해서 업데이트”한다.
  • 마지막 setState() 호출이 우선순위가 가장 높다.

6. 객체 State 업데이트하기

변경이란?

const [x, setX] = useState(0)
  • 숫자, 문자열, 불리언 등 원시 값(primitive)  불변(immutable) 하다.
  • setX(5)는 x 값을 0 → 5로 교체하는 것이지, 기존 값을 수정하는 것이 아니다
const [position, setPosition] = useState({ x: 0, y: 0 });
position.x = e.clientX;

위처럼 직접 값을 바꾸면, React는 이 변화를 감지하지 못함리렌더링이 발생하지 않음

setPosition({
  x: e.clientX,
  y: e.clientY
});

새로운 객체를 만들어서 넘겨줘야 React가 감지하고 리렌더링을 트리거함

전개 문법 (spread syntax) 사용

const [person, setPerson] = useState({
  firstName: 'Barbara',
  lastName: 'Hepworth',
  email: 'bhepworth@sculpture.com'
});
 
setPerson({
  ...person,
  firstName: e.target.value
}); //수정하고 싶은 필드만 골라 바꾸고, 나머지는 복사

 중첩된 객체 업데이트하기

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
  }
});
person.artwork.city = 'New Delhi'; // ❌ 안됨!
setPerson({
  ...person,
  artwork: {
    ...person.artwork,
    city: 'New Delhi'
  }
}); //✅ 올바른 방법

Immer 사용하기

  • 마치 직접 수정하는 것처럼 보이지만, 내부적으로는 불변성을 유지
  • 코드가 훨씬 간결하고 명확해짐
const [person, updatePerson] = useImmer({
  artwork: {
    city: 'Hamburg'
  }
});
updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

 

개념 설명
객체 state 직접 수정하지 말고, 항상 새 객체로 교체해야 함
불변성 React는 변경 여부를 감지할 수 있어야 함
전개 문법 { ...oldObj, changedProp: newVal }
Immer 중첩 객체 변경을 더 간결하게 표현할 수 있음

7. 배열 state 업데이트하기

배열 수정의 기본 원칙

  • 배열을 직접 수정하면 안된다! ( push, splice, sort 사용 시 주의)
  • 대신, 새로운 배열을 만들어 업데이트해야한다. (map, filter, ...spread 등 활용

동일한 객체/배열 참조는 React가 변경을 감지 못함

// ❌ 변경을 감지하지 못함
const nextList = list;
nextList[0].name = 'Alice';
setList(nextList); // React는 이걸 무시함!
 
// ✅ 참조가 바뀌었기 때문에 감지됨
const nextList = [...list];
nextList[0] = { ...nextList[0], name: 'Alice' };
setList(nextList);

→ React는 얕은 비교를 사용하기 때문에, 객체나 배열의 참조가 바뀌어야만 변경으로 인식함.

배열 state 변경 예시

// 항목 추가하기setArtists([...artists, { id: nextId++, name: name }]);//항목 삭제하기setArtists([...artists, { id: nextId++, name: name }]);// 항목 수정하기setArtists(
 artists.map(artist =>
 artist.id === targetId ? { ...artist, name: newName } : artist
 )
);

React가 변경을 감지하는 방식

React는 state가 이전과 다른 값일 때만 렌더링한다.

그래서 동일한 배열 참조를 유지하면 변경을 감지하지 못한다.

// ❌ 작동 X
artists.push(newArtist);
setArtists(artists);
// ✅ 작동함: 참조가 바뀜
setArtists([...artists, newArtist]);

요약

  • 배열 직접 수정 X → 새 배열 생성 
  • 객체 직접 수정 X → 새 객체로 교체 
  • map, filter, ...spread 등을 적극 활용하기
  • React는 “값”이 아닌 “참조”의 변경을 기준으로 렌더링 여부 판단함

https://ko.react.dev/learn/responding-to-events

1. 이벤트에 응답하기 (Responding to Events)


React에서는 브라우저 DOM 요소에 이벤트 핸들러(event handler) 를 연결하여 사용자 입력에 반응할 수 있다

JSX에서 이벤트 핸들러 추가하기

  • HTML처럼 onclick, onchange가 아니라 카멜케이스 onClick, onChange 사용
  • 값으로는 문자열이 아니라 함수 자체를 전달해야 함
<button onClick={handleClick}>Click me</button>

이벤트 핸들러 함수 만들기

  • 컴포넌트 안에 함수 정의 후 이벤트에 연결한다.합
  • 일반적으로 handleSomething 형식의 네이밍을 사용
function MyButton() {
  function handleClick() {
    alert('You clicked me!');
  }
 
  return <button onClick={handleClick}>Click me</button>;
}

이벤트 객체 사용하기

  • 이벤트 핸들러에 전달되는 첫 번째 인자는 이벤트 객체 (SyntheticEvent)
  • 예: onClick에서는 MouseEvent, onChange에서는 ChangeEvent 
function handleClick(e) {
  console.log(e); // SyntheticEvent
}

컴포넌트가 상태를 바꾸게 하기

  • 버튼 클릭 등의 이벤트를 통해 state 업데이트 가능
function MyButton() {
  const [count, setCount] = useState(0);
 
  function handleClick() {
    setCount(count + 1);
  }
 
  return <button onClick={handleClick}>Clicked {count} times</button>;
}

이벤트 핸들러에서 다른 함수 호출하기

  • 핸들러 안에서 다른 함수를 직접 호출하거나
  • 함수를 넘겨줄 수 있음
function handleClick(name) {
  alert(`Hello, ${name}`);
}
<button onClick={() => handleClick('Jaeeun')}>Greet</button>

주의할 점

  • JSX에서 ()를 붙이면 함수가 즉시 실행됨 → 핸들러로는 함수 참조만 전달

2. State: 컴포넌트의 기억 저장소

State는 컴포넌트의 기억 저장소이다. 컴포넌트가 사용자와의 상호작용 결과로 데이터를 저장하고, 그에 따라 화면을 다시 그리기 위해 사용된다.

React는 이러한 상태 정보를 컴포넌트 별로 관리할 수 있도록 useState 훅을 제공한다.

일반 변수의 한계

  • 다음 코드는 버튼 클릭 시 index를 증가시키고자 하지만 동작하지 않는다
let index = 0;
function handleClick() {
  index = index + 1;
}

왜 안 될까?

  1. 지역 변수는 렌더링 간 유지되지 않음
  2. React는 지역 변수 변화로 다시 렌더링하지 않음

따라서, UI가 반응형으로 업데이트되길 원한다면 반드시 state를 사용해야한다.

useState 훅으로 state 사용하기

  • 현재 값을 저장하는 state 변수 (index) 
  • state를 업데이트하고 렌더링을 유발하는 setter 함수 (setIndex)
import { useState } from 'react';
const [index, setIndex] = useState(0);  // 0은 초기값
 
function handleClick() {
  setIndex(index + 1);  // 새로운 값으로 갱신
}
  1. 초기 렌더링: [0, setIndex] 반환
  2. setIndex(1) 실행 시 → React가 다시 렌더링
  3. 이후 렌더링에선 [1, setIndex] 반환

⇒ state는 렌더링 간에도 값을 기억하고 UI를 일관되게 유지하는 핵심 요소이다

구조 분해 할당 문법

[value, setValue]는 배열 구조 분해 문법으로, 항상 두 개의 값이 들어있다.

  1. 현재 상태 값
  2. 상태 갱신 함수
const [value, setValue] = useState(initialValue);

여러 개의 state 변수 다루기

  • index: 현재 표시 중인 조각상 인덱스를 저장
  • showMore: 설명을 토글할지 여부를 제어하는 boolean
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);

각 state는 독립적으로 관리되므로, 관련성이 없을 경우 각각 나눠서 사용하는 것이 가독성과 유지보수에 좋다.

그러나 만약 여러 필드(state)를 한 번에 업데이트해야 하는 경우에는 객체 형태로 하나의 state로 묶는 것이 효율적이다.

상태 격리와 독립성

  • React의 state는 컴포넌트 인스턴스에 국한된 저장소이다.
  • 동일한 컴포넌트를 두 번 렌더링하면 각각의 복사본은 독립적인 state를 가진다.
  • 부모 컴포넌트는 자식의 state를 직접 알거나 수정할 수 없다.
  • 이는 state가 외부로부터 격리되고 캡슐화된다는 의미
<Gallery />
<Gallery />

=> 즉 위처럼 <Gallery />를 두 번 렌더링해도 각 갤러리는 독립적인 state를 유지한다. 하나의 갤러리에서 Next 버튼을 눌러도 다른 갤러리에는 영향이 없다.

3. 렌더링 그리고 커밋

React에서 컴포넌트를 화면에 표시하기 위한 전체 흐름은 다음 3단계로 이루어진다.

  1. 렌더링 트리거: 컴포넌트를 새로 렌더링해야 하는 이유가 발생
  2. 렌더링: React가 컴포넌트를 호출하여 JSX를 계산
  3. 커밋: 계산된 JSX를 실제 DOM에 반영

1. 렌더링 트리거

렌더링은 두 가지 경우에 발생한다.

  • 초기 렌더링 : 앱 시작 시 createRoot와 render()를 호출해 렌더링이 시작.
  • 상태 업데이트 useState set 수를 호출해 상태가 바뀌면, React는 해당 컴포넌트를 렌더링 대기열에 넣고 다시 렌더링을 시작한다.

 

2. React 컴포넌트 렌더링

React는 렌더링 트리거 후 컴포넌트를 호출하여 JSX를 계산한다. 렌더링 자체는 DOM을 변경하지 않으며, 순수한 계산 단계이다.

export default function Gallery() {
  return (
    <section>
      <h1>Inspiring Sculptures</h1>
      <Image />
      <Image />
      <Image />
    </section>
  );
}
function Image() {
  return (
    <img src="https://.." alt="..." />
  );
}

React는 Gallery  Image × 3 순서로 컴포넌트를 호출하며, 각 JSX 트리를 재귀적으로 계산

렌더링의 규칙 (순수성)

  • 같은 입력 → 같은 출력이어야 한다.
  • 기존 상태(state), 변수, 객체를 직접 수정하지 않아야 한다.
  • 이 규칙을 어기면 예측 불가능한 동작이 생길 수 있다.

개발 중에는 StrictMode를 통해 컴포넌트 렌더링을 2번 호출해서 이러한 실수를 잡아낼 수 있다.

3. DOM에 커밋

렌더링 단계가 끝나면 React는 변경된 부분을 DOM에 커밋(적용)

  • 초기 렌더링: appendChild() 등으로 DOM 노드를 생성
  • 리렌더링: 이전 결과와 비교해 필요한 최소 변경만 수행
export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}
  • time이 변경되면 <h1>만 새로 렌더링되고 <input>은 그대로 유지된다.
  • 그리고 time이 리렌더링 되어도 input에 텍스트가 사라지지 않는다

⇒ 마지막 변경 단계에서 React가 <input>이 JSX에서 동일한것이 확인되므로 React는 <input> 또는 value를 건드리지 않는다.

브라우저 페인트

렌더링이 완료되고 React가 DOM을 업데이트한 후 브라우저는 화면을 다시 그린다. 이 단계를 “브라우저 렌더링”이라고 하지만 이 문서의 나머지 부분에서 혼동을 피하고자 “페인팅”이라고 부른다.

4. 스냅샷으로서의 State

state 변경 흐름

  1. setState() 호출
  2. React가 다음 렌더링을 예약
  3. 컴포넌트를 다시 호출하여 JSX(스냅샷)를 반환
  4. React가 이 스냅샷에 맞게 실제 DOM을 업데이트

setNumber를 여러 번 호출해도 state는 1만 증가

<button onClick={() => {
  setNumber(number + 1);
  setNumber(number + 1);
  setNumber(number + 1);
}}>+3</button>
  • number가 0이면 → setNumber(0 + 1)이 3번 호출됨
  • 다음 렌더링에선 number가 1이 됨 (3이 아님)
  • 이유: number는 이벤트 핸들러 실행 시 "고정된 스냅샷"이기 때문

setTimeout 안의 state도 스냅샷으로 고정됨

setNumber(number + 5);
setTimeout(() => {
  alert(number); // 여전히 이전 렌더링의 number
}, 3000);
  • 3초 뒤에도 number는 여전히 "0"
  • 이유: setTimeout은 이벤트 핸들러 실행 당시의 스냅샷을 사용

이벤트 핸들러 안에서 state "고정" 확인

<form onSubmit={e => {
  e.preventDefault();
  setTimeout(() => {
    alert(`You said ${message} to ${to}`);
  }, 5000);
}}>
  • "To" 값을 Alice로 둔 채 Send를 클릭
  • 5초 이내에 To를 Bob으로 변경
  • Alert에는 여전히 "Alice" 가 출력됨
  • 이유: 핸들러가 생성될 당시의 state가 고정됨

요약

  • state는 일반 변수처럼 보이지만, 사실 스냅샷(snapshot)처럼 동작한다.
  • setState 호출 시, 현재 렌더링에는 영향을 주지 않고, 다음 렌더링을 큐(queue)에 넣는다.
  • React는 컴포넌트 외부에 state를 저장하고, 컴포넌트를 호출할 때 해당 시점의 state를 넘겨준다.
  • 이로 인해 이벤트 핸들러나 콜백 함수는 특정 시점의 state를 "고정"된 값으로 갖게 된다.

5. State 업데이트 큐

setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
  • 이 코드는 모든 setNumber(...)가 현재 렌더링 시점의 number 값(0)을 기준으로 동작함.
  • React는 이벤트 핸들러 실행이 끝난 후 batching(배치)해서 처리하므로,
    • setNumber(1)이 3번 큐에 들어가지만, 결과적으로는 같은 값으로 3번 대체하는 꼴이 됨.
  • 마지막 setNumber(1)만 반영됨

⇒ 최종결과 number = 1

setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
  • 이 코드는 각각의 setNumber 호출에 업데이터 함수를 사용하고 있음.
  • React는 이벤트가 끝난 뒤, 큐에 있는 함수를 순차적으로 실행해서 다음 상태를 계산함:
    1. n => n + 1  n = 0 → 1
    2. n => n + 1  n = 1 → 2
    3. n => n + 1  n = 2 → 3
  • 즉, 이전 상태를 기반으로 다음 상태를 계산하므로 업데이트가 누적됨.

⇒ 최종결과 number = 3

setNumber(number + 5);
setNumber(n => n + 1);
  • number + 5는 현재 상태(0)를 기준으로 계산된 정적 값이므로, setNumber(5)와 같음 → "5로 바꾸기"가 큐에 들어감.
  • 그 다음 setNumber(n => n + 1)은 5를 기반으로 1 증가시키는 함수로 동작함.
  • 결과적으로 React는 다음과 같이 처리함:
    1. 값 5로 교체
    2. 5를 받아서 +1 → 6

⇒ 최종결과 number = 6

setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
  • 큐에 다음과 같은 작업이 순서대로 들어감:
    1. setNumber(5) → 현재 상태인 0에 5 더해서 5로 설정
    2. setNumber(n => n + 1) → 그 직전 값 5를 받아 6으로 설정
    3. setNumber(42)  이전 모든 결과를 무시하고 42로 덮어씀
  • React는 마지막 값 설정(setNumber(42))이 들어오면 그 이전의 모든 계산 결과를 무시하고 덮어씀.

⇒ 최종결과 number = 42

요약

  • batching: React는 렌더링 성능을 높이기 위해 이벤트 핸들러 내에서 발생하는 여러 개의 setState 호출을 한 번의 렌더링으로 묶어 처리한다.
  • 업데이터 함수: setState(n => n + 1)처럼 이전 상태를 기반으로 다음 상태를 계산하는 함수를 전달할 수 있다. 이는 여러 번의 상태 변경을 순차적으로 적용할 수 있게 해준다.
  • 상태 설정 방식:
    • setState(5)는 “값을 대체”한다.
    • setState(n => n + 1)는 “값을 기반으로 연산해서 업데이트”한다.
  • 마지막 setState() 호출이 우선순위가 가장 높다.

6. 객체 State 업데이트하기

변경이란?

const [x, setX] = useState(0)
  • 숫자, 문자열, 불리언 등 원시 값(primitive)  불변(immutable) 하다.
  • setX(5)는 x 값을 0 → 5로 교체하는 것이지, 기존 값을 수정하는 것이 아니다
const [position, setPosition] = useState({ x: 0, y: 0 });
position.x = e.clientX;

위처럼 직접 값을 바꾸면, React는 이 변화를 감지하지 못함리렌더링이 발생하지 않음

setPosition({
  x: e.clientX,
  y: e.clientY
});

새로운 객체를 만들어서 넘겨줘야 React가 감지하고 리렌더링을 트리거함

전개 문법 (spread syntax) 사용

const [person, setPerson] = useState({
  firstName: 'Barbara',
  lastName: 'Hepworth',
  email: 'bhepworth@sculpture.com'
});
 
setPerson({
  ...person,
  firstName: e.target.value
}); //수정하고 싶은 필드만 골라 바꾸고, 나머지는 복사

 중첩된 객체 업데이트하기

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
  }
});
person.artwork.city = 'New Delhi'; // ❌ 안됨!
setPerson({
  ...person,
  artwork: {
    ...person.artwork,
    city: 'New Delhi'
  }
}); //✅ 올바른 방법

Immer 사용하기

  • 마치 직접 수정하는 것처럼 보이지만, 내부적으로는 불변성을 유지
  • 코드가 훨씬 간결하고 명확해짐
const [person, updatePerson] = useImmer({
  artwork: {
    city: 'Hamburg'
  }
});
updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

 

개념 설명
객체 state 직접 수정하지 말고, 항상 새 객체로 교체해야 함
불변성 React는 변경 여부를 감지할 수 있어야 함
전개 문법 { ...oldObj, changedProp: newVal }
Immer 중첩 객체 변경을 더 간결하게 표현할 수 있음

7. 배열 state 업데이트하기

배열 수정의 기본 원칙

  • 배열을 직접 수정하면 안된다! ( push, splice, sort 사용 시 주의)
  • 대신, 새로운 배열을 만들어 업데이트해야한다. (map, filter, ...spread 등 활용

동일한 객체/배열 참조는 React가 변경을 감지 못함

// ❌ 변경을 감지하지 못함
const nextList = list;
nextList[0].name = 'Alice';
setList(nextList); // React는 이걸 무시함!
 
// ✅ 참조가 바뀌었기 때문에 감지됨
const nextList = [...list];
nextList[0] = { ...nextList[0], name: 'Alice' };
setList(nextList);

→ React는 얕은 비교를 사용하기 때문에, 객체나 배열의 참조가 바뀌어야만 변경으로 인식함.

배열 state 변경 예시

// 항목 추가하기setArtists([...artists, { id: nextId++, name: name }]);//항목 삭제하기setArtists([...artists, { id: nextId++, name: name }]);// 항목 수정하기setArtists(
 artists.map(artist =>
 artist.id === targetId ? { ...artist, name: newName } : artist
 )
);

React가 변경을 감지하는 방식

React는 state가 이전과 다른 값일 때만 렌더링한다.

그래서 동일한 배열 참조를 유지하면 변경을 감지하지 못한다.

// ❌ 작동 X
artists.push(newArtist);
setArtists(artists);
// ✅ 작동함: 참조가 바뀜
setArtists([...artists, newArtist]);

요약

  • 배열 직접 수정 X → 새 배열 생성 
  • 객체 직접 수정 X → 새 객체로 교체 
  • map, filter, ...spread 등을 적극 활용하기
  • React는 “값”이 아닌 “참조”의 변경을 기준으로 렌더링 여부 판단함

https://ko.react.dev/learn/responding-to-events

1. 이벤트에 응답하기 (Responding to Events)


React에서는 브라우저 DOM 요소에 이벤트 핸들러(event handler) 를 연결하여 사용자 입력에 반응할 수 있다

JSX에서 이벤트 핸들러 추가하기

  • HTML처럼 onclick, onchange가 아니라 카멜케이스 onClick, onChange 사용
  • 값으로는 문자열이 아니라 함수 자체를 전달해야 함
<button onClick={handleClick}>Click me</button>

이벤트 핸들러 함수 만들기

  • 컴포넌트 안에 함수 정의 후 이벤트에 연결한다.합
  • 일반적으로 handleSomething 형식의 네이밍을 사용
function MyButton() {
  function handleClick() {
    alert('You clicked me!');
  }
 
  return <button onClick={handleClick}>Click me</button>;
}

이벤트 객체 사용하기

  • 이벤트 핸들러에 전달되는 첫 번째 인자는 이벤트 객체 (SyntheticEvent)
  • 예: onClick에서는 MouseEvent, onChange에서는 ChangeEvent 
function handleClick(e) {
  console.log(e); // SyntheticEvent
}

컴포넌트가 상태를 바꾸게 하기

  • 버튼 클릭 등의 이벤트를 통해 state 업데이트 가능
function MyButton() {
  const [count, setCount] = useState(0);
 
  function handleClick() {
    setCount(count + 1);
  }
 
  return <button onClick={handleClick}>Clicked {count} times</button>;
}

이벤트 핸들러에서 다른 함수 호출하기

  • 핸들러 안에서 다른 함수를 직접 호출하거나
  • 함수를 넘겨줄 수 있음
function handleClick(name) {
  alert(`Hello, ${name}`);
}
<button onClick={() => handleClick('Jaeeun')}>Greet</button>

주의할 점

  • JSX에서 ()를 붙이면 함수가 즉시 실행됨 → 핸들러로는 함수 참조만 전달

2. State: 컴포넌트의 기억 저장소

State는 컴포넌트의 기억 저장소이다. 컴포넌트가 사용자와의 상호작용 결과로 데이터를 저장하고, 그에 따라 화면을 다시 그리기 위해 사용된다.

React는 이러한 상태 정보를 컴포넌트 별로 관리할 수 있도록 useState 훅을 제공한다.

일반 변수의 한계

  • 다음 코드는 버튼 클릭 시 index를 증가시키고자 하지만 동작하지 않는다
let index = 0;
function handleClick() {
  index = index + 1;
}

왜 안 될까?

  1. 지역 변수는 렌더링 간 유지되지 않음
  2. React는 지역 변수 변화로 다시 렌더링하지 않음

따라서, UI가 반응형으로 업데이트되길 원한다면 반드시 state를 사용해야한다.

useState 훅으로 state 사용하기

  • 현재 값을 저장하는 state 변수 (index) 
  • state를 업데이트하고 렌더링을 유발하는 setter 함수 (setIndex)
import { useState } from 'react';
const [index, setIndex] = useState(0);  // 0은 초기값
 
function handleClick() {
  setIndex(index + 1);  // 새로운 값으로 갱신
}
  1. 초기 렌더링: [0, setIndex] 반환
  2. setIndex(1) 실행 시 → React가 다시 렌더링
  3. 이후 렌더링에선 [1, setIndex] 반환

⇒ state는 렌더링 간에도 값을 기억하고 UI를 일관되게 유지하는 핵심 요소이다

구조 분해 할당 문법

[value, setValue]는 배열 구조 분해 문법으로, 항상 두 개의 값이 들어있다.

  1. 현재 상태 값
  2. 상태 갱신 함수
const [value, setValue] = useState(initialValue);

여러 개의 state 변수 다루기

  • index: 현재 표시 중인 조각상 인덱스를 저장
  • showMore: 설명을 토글할지 여부를 제어하는 boolean
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);

각 state는 독립적으로 관리되므로, 관련성이 없을 경우 각각 나눠서 사용하는 것이 가독성과 유지보수에 좋다.

그러나 만약 여러 필드(state)를 한 번에 업데이트해야 하는 경우에는 객체 형태로 하나의 state로 묶는 것이 효율적이다.

상태 격리와 독립성

  • React의 state는 컴포넌트 인스턴스에 국한된 저장소이다.
  • 동일한 컴포넌트를 두 번 렌더링하면 각각의 복사본은 독립적인 state를 가진다.
  • 부모 컴포넌트는 자식의 state를 직접 알거나 수정할 수 없다.
  • 이는 state가 외부로부터 격리되고 캡슐화된다는 의미
<Gallery />
<Gallery />

=> 즉 위처럼 <Gallery />를 두 번 렌더링해도 각 갤러리는 독립적인 state를 유지한다. 하나의 갤러리에서 Next 버튼을 눌러도 다른 갤러리에는 영향이 없다.

3. 렌더링 그리고 커밋

React에서 컴포넌트를 화면에 표시하기 위한 전체 흐름은 다음 3단계로 이루어진다.

  1. 렌더링 트리거: 컴포넌트를 새로 렌더링해야 하는 이유가 발생
  2. 렌더링: React가 컴포넌트를 호출하여 JSX를 계산
  3. 커밋: 계산된 JSX를 실제 DOM에 반영

1. 렌더링 트리거

렌더링은 두 가지 경우에 발생한다.

  • 초기 렌더링 : 앱 시작 시 createRoot와 render()를 호출해 렌더링이 시작.
  • 상태 업데이트 useState set 수를 호출해 상태가 바뀌면, React는 해당 컴포넌트를 렌더링 대기열에 넣고 다시 렌더링을 시작한다.

 

2. React 컴포넌트 렌더링

React는 렌더링 트리거 후 컴포넌트를 호출하여 JSX를 계산한다. 렌더링 자체는 DOM을 변경하지 않으며, 순수한 계산 단계이다.

export default function Gallery() {
  return (
    <section>
      <h1>Inspiring Sculptures</h1>
      <Image />
      <Image />
      <Image />
    </section>
  );
}
function Image() {
  return (
    <img src="https://.." alt="..." />
  );
}

React는 Gallery  Image × 3 순서로 컴포넌트를 호출하며, 각 JSX 트리를 재귀적으로 계산

렌더링의 규칙 (순수성)

  • 같은 입력 → 같은 출력이어야 한다.
  • 기존 상태(state), 변수, 객체를 직접 수정하지 않아야 한다.
  • 이 규칙을 어기면 예측 불가능한 동작이 생길 수 있다.

개발 중에는 StrictMode를 통해 컴포넌트 렌더링을 2번 호출해서 이러한 실수를 잡아낼 수 있다.

3. DOM에 커밋

렌더링 단계가 끝나면 React는 변경된 부분을 DOM에 커밋(적용)

  • 초기 렌더링: appendChild() 등으로 DOM 노드를 생성
  • 리렌더링: 이전 결과와 비교해 필요한 최소 변경만 수행
export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}
  • time이 변경되면 <h1>만 새로 렌더링되고 <input>은 그대로 유지된다.
  • 그리고 time이 리렌더링 되어도 input에 텍스트가 사라지지 않는다

⇒ 마지막 변경 단계에서 React가 <input>이 JSX에서 동일한것이 확인되므로 React는 <input> 또는 value를 건드리지 않는다.

브라우저 페인트

렌더링이 완료되고 React가 DOM을 업데이트한 후 브라우저는 화면을 다시 그린다. 이 단계를 “브라우저 렌더링”이라고 하지만 이 문서의 나머지 부분에서 혼동을 피하고자 “페인팅”이라고 부른다.

4. 스냅샷으로서의 State

state 변경 흐름

  1. setState() 호출
  2. React가 다음 렌더링을 예약
  3. 컴포넌트를 다시 호출하여 JSX(스냅샷)를 반환
  4. React가 이 스냅샷에 맞게 실제 DOM을 업데이트

setNumber를 여러 번 호출해도 state는 1만 증가

<button onClick={() => {
  setNumber(number + 1);
  setNumber(number + 1);
  setNumber(number + 1);
}}>+3</button>
  • number가 0이면 → setNumber(0 + 1)이 3번 호출됨
  • 다음 렌더링에선 number가 1이 됨 (3이 아님)
  • 이유: number는 이벤트 핸들러 실행 시 "고정된 스냅샷"이기 때문

setTimeout 안의 state도 스냅샷으로 고정됨

setNumber(number + 5);
setTimeout(() => {
  alert(number); // 여전히 이전 렌더링의 number
}, 3000);
  • 3초 뒤에도 number는 여전히 "0"
  • 이유: setTimeout은 이벤트 핸들러 실행 당시의 스냅샷을 사용

이벤트 핸들러 안에서 state "고정" 확인

<form onSubmit={e => {
  e.preventDefault();
  setTimeout(() => {
    alert(`You said ${message} to ${to}`);
  }, 5000);
}}>
  • "To" 값을 Alice로 둔 채 Send를 클릭
  • 5초 이내에 To를 Bob으로 변경
  • Alert에는 여전히 "Alice" 가 출력됨
  • 이유: 핸들러가 생성될 당시의 state가 고정됨

요약

  • state는 일반 변수처럼 보이지만, 사실 스냅샷(snapshot)처럼 동작한다.
  • setState 호출 시, 현재 렌더링에는 영향을 주지 않고, 다음 렌더링을 큐(queue)에 넣는다.
  • React는 컴포넌트 외부에 state를 저장하고, 컴포넌트를 호출할 때 해당 시점의 state를 넘겨준다.
  • 이로 인해 이벤트 핸들러나 콜백 함수는 특정 시점의 state를 "고정"된 값으로 갖게 된다.

5. State 업데이트 큐

setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
  • 이 코드는 모든 setNumber(...)가 현재 렌더링 시점의 number 값(0)을 기준으로 동작함.
  • React는 이벤트 핸들러 실행이 끝난 후 batching(배치)해서 처리하므로,
    • setNumber(1)이 3번 큐에 들어가지만, 결과적으로는 같은 값으로 3번 대체하는 꼴이 됨.
  • 마지막 setNumber(1)만 반영됨

⇒ 최종결과 number = 1

setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
  • 이 코드는 각각의 setNumber 호출에 업데이터 함수를 사용하고 있음.
  • React는 이벤트가 끝난 뒤, 큐에 있는 함수를 순차적으로 실행해서 다음 상태를 계산함:
    1. n => n + 1  n = 0 → 1
    2. n => n + 1  n = 1 → 2
    3. n => n + 1  n = 2 → 3
  • 즉, 이전 상태를 기반으로 다음 상태를 계산하므로 업데이트가 누적됨.

⇒ 최종결과 number = 3

setNumber(number + 5);
setNumber(n => n + 1);
  • number + 5는 현재 상태(0)를 기준으로 계산된 정적 값이므로, setNumber(5)와 같음 → "5로 바꾸기"가 큐에 들어감.
  • 그 다음 setNumber(n => n + 1)은 5를 기반으로 1 증가시키는 함수로 동작함.
  • 결과적으로 React는 다음과 같이 처리함:
    1. 값 5로 교체
    2. 5를 받아서 +1 → 6

⇒ 최종결과 number = 6

setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
  • 큐에 다음과 같은 작업이 순서대로 들어감:
    1. setNumber(5) → 현재 상태인 0에 5 더해서 5로 설정
    2. setNumber(n => n + 1) → 그 직전 값 5를 받아 6으로 설정
    3. setNumber(42)  이전 모든 결과를 무시하고 42로 덮어씀
  • React는 마지막 값 설정(setNumber(42))이 들어오면 그 이전의 모든 계산 결과를 무시하고 덮어씀.

⇒ 최종결과 number = 42

요약

  • batching: React는 렌더링 성능을 높이기 위해 이벤트 핸들러 내에서 발생하는 여러 개의 setState 호출을 한 번의 렌더링으로 묶어 처리한다.
  • 업데이터 함수: setState(n => n + 1)처럼 이전 상태를 기반으로 다음 상태를 계산하는 함수를 전달할 수 있다. 이는 여러 번의 상태 변경을 순차적으로 적용할 수 있게 해준다.
  • 상태 설정 방식:
    • setState(5)는 “값을 대체”한다.
    • setState(n => n + 1)는 “값을 기반으로 연산해서 업데이트”한다.
  • 마지막 setState() 호출이 우선순위가 가장 높다.

6. 객체 State 업데이트하기

변경이란?

const [x, setX] = useState(0)
  • 숫자, 문자열, 불리언 등 원시 값(primitive)  불변(immutable) 하다.
  • setX(5)는 x 값을 0 → 5로 교체하는 것이지, 기존 값을 수정하는 것이 아니다
const [position, setPosition] = useState({ x: 0, y: 0 });
position.x = e.clientX;

위처럼 직접 값을 바꾸면, React는 이 변화를 감지하지 못함리렌더링이 발생하지 않음

setPosition({
  x: e.clientX,
  y: e.clientY
});

새로운 객체를 만들어서 넘겨줘야 React가 감지하고 리렌더링을 트리거함

전개 문법 (spread syntax) 사용

const [person, setPerson] = useState({
  firstName: 'Barbara',
  lastName: 'Hepworth',
  email: 'bhepworth@sculpture.com'
});
 
setPerson({
  ...person,
  firstName: e.target.value
}); //수정하고 싶은 필드만 골라 바꾸고, 나머지는 복사

 중첩된 객체 업데이트하기

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
  }
});
person.artwork.city = 'New Delhi'; // ❌ 안됨!
setPerson({
  ...person,
  artwork: {
    ...person.artwork,
    city: 'New Delhi'
  }
}); //✅ 올바른 방법

Immer 사용하기

  • 마치 직접 수정하는 것처럼 보이지만, 내부적으로는 불변성을 유지
  • 코드가 훨씬 간결하고 명확해짐
const [person, updatePerson] = useImmer({
  artwork: {
    city: 'Hamburg'
  }
});
updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

 

개념 설명
객체 state 직접 수정하지 말고, 항상 새 객체로 교체해야 함
불변성 React는 변경 여부를 감지할 수 있어야 함
전개 문법 { ...oldObj, changedProp: newVal }
Immer 중첩 객체 변경을 더 간결하게 표현할 수 있음

7. 배열 state 업데이트하기

배열 수정의 기본 원칙

  • 배열을 직접 수정하면 안된다! ( push, splice, sort 사용 시 주의)
  • 대신, 새로운 배열을 만들어 업데이트해야한다. (map, filter, ...spread 등 활용

동일한 객체/배열 참조는 React가 변경을 감지 못함

// ❌ 변경을 감지하지 못함
const nextList = list;
nextList[0].name = 'Alice';
setList(nextList); // React는 이걸 무시함!
 
// ✅ 참조가 바뀌었기 때문에 감지됨
const nextList = [...list];
nextList[0] = { ...nextList[0], name: 'Alice' };
setList(nextList);

→ React는 얕은 비교를 사용하기 때문에, 객체나 배열의 참조가 바뀌어야만 변경으로 인식함.

배열 state 변경 예시

// 항목 추가하기setArtists([...artists, { id: nextId++, name: name }]);//항목 삭제하기setArtists([...artists, { id: nextId++, name: name }]);// 항목 수정하기setArtists(
 artists.map(artist =>
 artist.id === targetId ? { ...artist, name: newName } : artist
 )
);

React가 변경을 감지하는 방식

React는 state가 이전과 다른 값일 때만 렌더링한다.

그래서 동일한 배열 참조를 유지하면 변경을 감지하지 못한다.

// ❌ 작동 X
artists.push(newArtist);
setArtists(artists);
// ✅ 작동함: 참조가 바뀜
setArtists([...artists, newArtist]);

요약

  • 배열 직접 수정 X → 새 배열 생성 
  • 객체 직접 수정 X → 새 객체로 교체 
  • map, filter, ...spread 등을 적극 활용하기
  • React는 “값”이 아닌 “참조”의 변경을 기준으로 렌더링 여부 판단함