본문 바로가기
3주차

왜 콘솔에 값이 이상하게 찍히지?

by jstar00 2025. 5. 2.

setState()에 대해 제대로 이해하지 못했기 때문입니다! 제 얘기 맞아요!

라고 매우매우 강조하네요.

안녕하세요 36기 웹 임지성입니다.

이번 주제에 대한 아티클을 작성하기 위해 여러 블로그와 유튜브 영상 등을 찾아보니 setState()가 '동기함수'냐 '비동기함수'냐에 대한 갑론을박이 상당하더라고요.. 대체적으로 'setState() 자체는 동기함수지만 상태는 렌더링 타이밍에 반영되므로 비동기적으로 처리된다'는 의견이 주류인 것으로 보였지만 명확히 정리되지는 않은 듯 해요.

 

따라서 이번 아티클에서는 setState()가 동기함수냐 비동기함수냐! 에 초점을 두고 글을 작성하지는 않을 거고,

제가 여러 번 겪었던 문제지만 그때마다 귀찮아서 '나중에 알아봐야지~' 하고 미뤄버렸던 'setState()와 console.log()를 함께 사용했을 때, 콘솔에 값이 이상하게 찍히는 이유'와 '해결방법'에 대해 알아보려고 합니다!

 


 

예시

콘솔에 값이 이상하게 찍힌다는 건 결국 setState()가 원하는 대로 동작하지 않았다는 거겠죠? 그 예시를 보여드리기 위해 제 유튜브 선생님인 '코딩애플'님의 블로그에서 예제를 가져와 직접 실행해봤습니다.

import { useState } from "react";

function App() {
  let [count, setCount] = useState(0);
  let [age, setAge] = useState(20);

  const handleOnClick = () => {
    setCount(count + 1);
    console.log(count);
    if (count < 3) {
      setAge(age + 1);
      console.log(age);
    }
  };

  return (
    <div>
      <div>안녕하세요 전 {age}</div>
      <button type="button" onClick={handleOnClick}>
        누르면 한 살 더 먹음
      </button>
    </div>
  );
}

export default App;

 

버튼을 누를 때마다 count와 age를 +1씩 하지만, 만약 count가 3 이상이라면 age라는 state는 더이상 1을 더하지 않도록 구현한 코드입니다. 즉, 버튼을 3번째 누를 때부터는 age를 더하면 안되고, 그럼 age는 22에서 멈춰야겠죠?

근데 실제로 위 코드를 실행해보면, age가 23까지 늘어나는 걸 확인할 수 있습니다..! 심지어 화면에 렌더링된 값과 콘솔에 나타난 age값이 다르기까지 하죠. 

 

위 예시로 저는 크게 세가지 의문이 들었습니다.

1. 왜 화면에서는 age가 23까지 증가하는 거야? 분명 if문에서 걸려서 +1이 되면 안될텐데?

2. 왜 setCount(), setAge() 다음에 console.log()로 count와 age를 출력했는데 둘 다 +1이 반영이 안된 값으로 찍혀?

3. 왜 화면에 렌더링된 값과 콘솔에 찍히는 값이 달라?

 

이 세 가지 상황 모두 같은 이유로 발생합니다.

React의 setState는 비동기적으로 작동해서, 상태를 즉시 업데이트하지 않고 리렌더링 과정에서 반영하기 때문입니다.

 

조금 더 자세히 알아볼까요?

1. 왜 화면에서는 age가 23까지 증가하는 거야?

setCount(count + 1);
if (count < 3) {
	setAge(age + 1);
}

이 코드에서 setCount(count + 1)을 실행하면 실제로 count가 그 자리에서 즉시 증가하지 않습니다. 즉, if (count < 3) 조건은 이전 상태값인 count에 대해 실행되는 것이죠. 

예를 들어 버튼을 3번 누를 때마다 count값의 변화는 이렇게 실행됩니다.

클릭 수 현재 count if 조건 setAge 실행 여부
1 0 true 실행 (age = 21)
2 1 true 실행 (age = 22)
3 2 true 실행 (age = 23)
4 3 false 실행 안 함

 

2. 왜 setCount(), setAge() 다음에 console.log()로 count와 age를 출력했는데 둘 다 +1이 반영이 안된 값으로 찍혀?

이 역시 React의 setState가 비동기적으로 작동해서 상태를 즉시 업데이트하지 않고, 리렌더링 과정에서 반영하기 때문이죠.

setAge(age + 1); // age가 바로 바뀌지 않음!
console.log(age); // 아직 이전 값

console.log(age)는 업데이트 요청(setAge) 이전의 age값을 출력합니다. 실제로는 "age를 21로 업데이트해줘!" 라고 요청만 했지, 그 자리에서 바로 age가 21로 바뀐게 아니에요.

 

3. 왜 화면에 렌더링된 값과 콘솔에 찍히는 값이 달라?

다시 한 번 1번, 2번과 같은 얘기입니다. 결국 React의 상태 업데이트가 비동기적이며, 업데이트가 반영되는 시점이 다르기 때문입니다.

- 콘솔 로그: 업데이트 요청 직후의 값을 찍음(아직 업데이트된 값이 반영 안됨)

- 화면 렌더링: 업데이트가 완료된 이후의 값을 보여줌

 

코딩애플 선생님은 이렇게 정리했습니다.

 


해결 방법

async하게 동작하는게 문제니까, 위 코드를 sync스럽게 만들면 해결되겠죠! 코드가 이런 순서로 실행돼야 합니다.

 

1. count라는 state가 변경되고 난 뒤,

2. age도 변경할게요

 

useEffect를 사용하면 특정 state가 변경될 때 useEffect를 실행할 수 있다는 특징을 통해 위 문제를 해결해볼까요?

 

import { useEffect } from "react";
import { useState } from "react";

function App() {
  let [count, setCount] = useState(0);
  let [age, setAge] = useState(20);

  const handleOnClick = () => {
    setCount(count + 1);
    console.log(count);
  };

  useEffect(() => {
    if (count != 0 && count < 3) { 
      setAge(age + 1);
      console.log(age);
    }
  }, [count]);

  return (
    <div>
      <div>안녕하세요 전 {age}</div>
      <button type="button" onClick={handleOnClick}>
        누르면 한 살 더 먹음
      </button>
    </div>
  );
}

export default App;

 

이렇게 useEffect()를 사용하니까 원하던 대로 렌더링되네요! 물론 console.log로 찍은 결과는 여전히 아까처럼 변경된 값을 즉시 반영하지는 못해요. 이 문제는

  const handleOnClick = () => {
    setCount(prev => {
      const next = prev + 1;
      console.log("next count:", next);
      return next;
    });
  };

이렇게 setState()에서 함수형으로 업데이트하면 해결됩니다.

+) 단, setCount(count + 1)과 같이 업데이트 할 때와 다르게 콘솔에 "next count: _"가 두 번씩 출력되는걸 확인할 수 있어요. 이건 React의 Strict Mode(<React.StrictMode>)가 의도적으로 일부 함수 컴포너트들을 두 번 호출하기 때문이고, 배포 환경에서는 발생하지 않는 문제이므로 크게 신경쓰지는 않아도 될 것 같네요.

 

추가적으로, useEffect의 if문에 count != 0 이라는 조건을 넣은 이유는, useEffect()가 페이지가 처음 로드될 때 의도치 않게 한 번 실행되는걸 막기 위해서입니다. count가 0일 때는, 즉 페이지가 처음 로드됐을 때는 if문 안의 코드를 동작시키지 않게 구현한거죠.

React 중급자들은 좀 더 고급지게, useRef()를 사용해 첫 렌더링을 막을 수도 있습니다!

 


 

마지막으로 'batch'와 'React가 setState를 비동기적으로 실행하는 이유' 대해 간단히 설명하며 아티클을 마무리하겠습니다!

 

batch여러 개의 setState 호출을 한꺼번에 묶어서 처리하는 걸 의미해요.

const handleClick = () => {
  setCount(count + 1);
  setAge(age + 1);
  console.log("렌더링!");
};

위 코드에서 setCount()와 setAge()를 연달아 호출한다면 각각 렌더링을 일으킬 거라고 생각할 수 있는데, React는 Batching을 통해 setState() 함수들을 한 번에 처리해서 렌더링도 딱 한 번만 일어나게 만듭니다. 이를 통해 성능 최적화가 가능하고, 렌더링 결과를 더 일관되게 만들 수 있죠. 

 

React가 setState를 비동기적으로 실행하는 이유는, 

1. 여러 상태 변경을 한꺼번에 처리하기 위해(Batching)

setState()가 동기적으로 작동한다면, 상태가 바뀔 때마다 컴포넌트가 즉시 리렌더링 해야 합니다. 하지만 React는 setState를 즉시 실행하지 않고, async하게 예약해두는 방식으로 작동해요.

=> 이를 통해 불필요한 연산과 리렌더링을 방지하는 거죠!

 

2. React 내부에서의 스케줄링과 최적화 구조에 맞추기 위해

React는 내부적으로 fiber 아키텍처를 통해 렌더링을 스케줄링합니다. 상태 업데이트가 즉시 처리되면 이런 스케줄링 구조와 충돌할 수 있다는 문제가 발생해요. 

+) fiber 아키텍처에 대한 설명은 아래 블로그에 잘 설명돼있습니다.

https://velog.io/@jay/setStateisnotasync

 

콘솔로그가 이상한건 setState가 비동기 함수여서가 아닙니다. (feat: fiber architecture)

코드는 짤 줄 알지만 설명하기 버거운 당신을 위하여

velog.io

 


이렇게 setState()의 동작 방식과, 콘솔 로그가 이상하게 찍히는 이유에 대해 알아봤습니다!

오랜 궁금증이었는데 드디어 해결하네요ㅎㅎ