인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

jinio님의 프로필 이미지

작성한 질문수

처음 만난 리액트(React)

(실습) Hooks 사용해보기

상태가 업데이트될때 컴포넌트 최상단의 console.log 코드가 두번 실행되는 이유가 궁금합니다.

작성

·

69

·

수정됨

1

import React, { useEffect, useState } from "react";
import useCounter from "./useCounter";

const MAX_CAPACITY = 10;

export default function Accommodate() {
  const [isFull, setIsFull] = useState(false);
  const [count, increaseCount, decreaseCount] = useCounter(0);
  console.log(count,"rendor Accommodate", new Date().getMilliseconds())

  useEffect(() => {
    console.log("====================");
    console.log("useEffect() is called.");
    console.log("isFull:", isFull);
  });

  useEffect(() => {
    setIsFull(count >= MAX_CAPACITY);
    console.log("Current count value:", count);
  }, [count])

  return <>
    <button onClick={increaseCount}>입장</button>
    <button onClick={decreaseCount}>퇴장</button>
    {isFull && <p>정원이 가득 찼습니다.</p>}
  </>;
}

안녕하세요! 실습 진행 중에 이해가 안되는 부분이 있어 질문드립니다!

우선 컴포넌트 내부에 포함된 state가 변경될때 컴포넌트가 재렌더링 되는게 맞을까요?

그게 맞다면, 다음과 같은 순서로 실행된다고 생각했는데요.

  1. 입장 버튼 클릭

  2. increaseCount가 실행되면서 count 상태가 변경됨

  3. Accommodate 컴포넌트가 재렌더링

    1. 첫번째 useEffect함수가 실행

    2. 두번째 useEffect함수가 실행

       

그리고 위 순서에 따라 console에는 다음과 같이 출력될 것이라고 예상했는데, 실제로는 캡쳐화면처럼 출력되더라구요.

어떤 이유로 컴포넌트 최상단에 있는 console.log(count,"rendor Accommodate", new Date().getMilliseconds()) 코드가 두번 실행되는 건지 이해가 안됩니다.ㅠ

제가 놓치고 있는 부분이 많은 것 같은데 어디서 부터 놓친건지 잘 정리가 되지 않네요!ㅠㅠ

0 'rendor Accommodate' 991
====================
useEffect() is called.
isFull: false
Current count value: 0
1 'rendor Accommodate' 430
====================
useEffect() is called.
isFull: false
Current count value: 1
image.png

 

답변 2

1

Inje Lee (소플)님의 프로필 이미지
Inje Lee (소플)
지식공유자

안녕하세요, 소플입니다.

우선 현재 겪고 계신 상황을 자세하게 설명해주셔서 감사합니다.

AI가 답변해준 StrictMode는 처음 컴포넌트 마운트 시에만 해당되는 내용이기 때문에, 지금 질문해주신 내용과는 다르다는 점을 먼저 말씀드립니다.

 

상황을 정리해보면 count가 변경됨으로써 재렌더링이 발생하고,

그 이후에 count를 의존성 배열에 갖고 있는 useEffect() 훅에서 아래와 같은 코드가 실행됩니다.

setIsFull(count >= MAX_CAPACITY);

이 때 isFull의 값이 변경되면 재렌더링이 발생하지만, 변경되지 않더라도 내부적으로 Virtual DOM Tree를 생성하기 위해 함수 컴포넌트가 호출되면서 React element를 새로 만드는 경우가 있습니다.

그래서 그런 경우에 로그가 두 번 출력되는 것이라고 이해하시면 됩니다.

 

아래에 더 간단한 예시코드를 첨부해두었으니 한 번 테스트 해보시기 바랍니다.

이 코드에서도 동일한 값의 버튼을 누를 경우 콘솔 로그가 출력되지 않을 것으로 기대되지만,

일부 상황에서는 콘솔 로그가 출력되는 것을 확인할 수 있습니다.

import { useState } from "react";

function MyComponent() {
  console.log("컴포넌트 렌더링!");

  const [state, setState] = useState(false);

  return (
    <div>
      <p>State: {state.toString()}</p>
      <button onClick={() => setState(false)}>false로 설정</button>
      <button onClick={() => setState(true)}>true로 설정</button>
    </div>
  );
}

export default MyComponent;

 

하지만 콘솔 로그가 출력(React element가 생성)된다는 것이 매번 화면이 갱신된다는 의미는 아닙니다.

리액트는 Virtual DOM을 사용하여 변경된 부분만을 계산하고 업데이트하는데, 이 때 상태 값이 변하지 않으면 리액트는 변경 사항이 없다고 판단하고 실제 DOM 업데이트를 건너뛰게 되기 때문입니다.

 

나중에는 콘솔 로그를 찍어서 확인하기 보다는 React Devtools의 Profiler 기능을 사용해서 더 정확하게 재렌더링이 몇 번 발생하는지 확인해보시면 좋습니다!

 

감사합니다.

jinio님의 프로필 이미지
jinio
질문자

소플님, 감사합니다!
답변 주신 내용 덕분에 이해하는데 많은 도움이 되었는데, 추가로 몇가지 질문이 있어서 답글 드립니다!

image.png

1. React Devtools의 Profiler기능을 사용해서 확인해보면 입장 버튼을 눌렀을때 위와 같이 보이는데 이게 한번만 재렌더링(화면갱신)된거라고 이해하면될까요?


2. React element가 생성(함수호출)되는것과 컴포넌트가 재렌더링(화면갱신) 된다는 다른 의미라고 생각하면 될까요?

3. 답변 주신 내용에 따라 이해한게 맞다면, 실행 흐름이 다음과 같이 진행되는 걸까요?

  1. 입장 버튼 클릭

     

  2. increaseCount가 실행되면서 count 상태가 변경

  3. Accommodate 함수 호출

    1. 함수 컴포넌트 최상단의 console.log 출력
      1 'rendor Accommodate' 430

    2. 첫번째 useEffect 함수 실행
      ====================
      useEffect() is called.
      isFull: false

    3. 두번째 useEffect 함수 실행

       

      1. setIsFull로 인해 컴포넌트가 재렌더링 될 예정 (대기)

      2. 기존 실행 흐름에 의해 console.log 출력
        Current count value: 1

  4. setIsFull로 인해 대기중이던 Accommodate 함수 실행

    1. 함수 컴포넌트 최상단의 console.log 출력
      1 'rendor Accommodate' 441

    2. 이미 이전 실행 흐름에서 화면상 업데이트가 완료(재렌더링 완료)되었고, count 상태 변경이 없으므로 useEffect 실행되지 않음

       

Inje Lee (소플)님의 프로필 이미지
Inje Lee (소플)
지식공유자

  1. Profiler에서 보이는 Commit이 실제 브라우저의 DOM에 반영된 것이라고 보면 됩니다. 렌더링이라는 과정을 크게 보면 리액트가 element를 만드는 과정까지도 포함해서 이야기 할 수 있지만, 만약 실제 화면에 반영되는 것만을 렌더링이라고 표현한다면 첨부해주신 그림에서는 한 번만 재렌더링 되었다고 볼 수 있습니다.

  2. 리액트가 element를 생성하는 과정과 실제 브라우저의 DOM에 반영되는 것은 각각 다른 작업입니다. 이를 제대로 이해하기 위해서는 React Fiber 아키텍처에 대한 아래 글을 한 번 읽어보시면 도움이 되실 것 같습니다.
    https://d2.naver.com/helloworld/2690975

  3. 대략적으로 아래와 같은 흐름이라고 이해하시면 됩니다.
    1. 입장 버튼 클릭
    2. count 상태 업데이트
    3. Accommodate 호출 > 리액트 element 생성 > 트리 재조정(Reconciliation) > 렌더링(Commit) > 첫 번째 useEffect 실행 > 두 번째 useEffect 실행 > setIsFull() 호출 > 리액트 element 생성 > 트리 재조정(Reconciliation) > 변경사항 없으므로 Commit 안 함

     

0

인프런 AI 인턴님의 프로필 이미지

안녕하세요, 인프런 AI 인턴입니다. jinio님.

질문하신 컴포넌트 최상단의 console.log 코드가 두번 실행되는 이유는 React의 개발 모드로 인한 것입니다. 개발 모드에서는 React가 컴포넌트가 올바르게 동작하는지 확인하기 위해 의도적으로 두 번씩 렌더링하는 StrictMode가 적용됩니다. 이 StrictModeReact.StrictMode로 감싸진 컴포넌트에서 개발 과정에서의 버그를 조기 발견하도록 돕는 기능입니다.

정리하자면:

  1. 첫 번째 렌더링: 상태가 변경되어 처음으로 컴포넌트를 렌더링합니다. 여기서 console.log가 실행됩니다.
  2. 두 번째 렌더링: React의 StrictMode에 의해 동일한 컴포넌트를 추가로 렌더링하여 잠재적 부작용을 체크합니다. 이로 인해 console.log가 다시 실행됩니다.

해결책

만약 이러한 두 번의 렌더링이 혼란스럽거나 불필요하다고 판단되시면, 개발 모드에서 테스트를 할 때 index.js 파일에서 React.StrictMode를 제거하면 됩니다. 하지만 실제 배포 환경에서는 이 모드가 비활성화되기 때문에 두 번 렌더링 되지 않습니다.

리액트의 이 동작에 대한 더 자세한 설명은 아래 문서를 참고하세요:
- React Docs: StrictMode

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

jinio님의 프로필 이미지

작성한 질문수

질문하기