React 19

[React] React 19를 알아보자! (2) 프로젝트 setting & useState의 파생 상태와 지연 초기화

student0 2025. 5. 7. 10:52

React Projeact Setting

React 공식 문서에서는 Vite 사용을 권장합니다. 작년만 해도 CRA였는데 CRA는 이제 지원되지 않습니다..ㅠ 빠르게 새로운 친구인 Vite에 적응해야죠! 저는 패키지 매니저로 pnpm을 사용하기 때문에 pnpm을 이용하여 프로젝트 셋팅을 할 예정입니다. pnpm을 사용하는 이유는 추후에 따로 정리해 올리겠습니다.

npm install -g pnpm@latest-10
pnpm create vite
pnpm install

 

이렇게 설치하기 전에 컴퓨터나 노트북에 Node.js가 없다면 Node.js를 설치해 줘야 합니다.
그냥 LTS를 설치해 주어도 되지만, 이후 버전을 변경해야 할 경우를 고려해 nvm을 사용해서 node.js를 설치해주는 것이 좋습니다.

 

nvm 설치 및 node 설치 - 사용법(mac&windows)

1. windows 에서 설치 아래 경로로 이동해서 Windows용 nvm설치 파일을 다운로드 한다. nvm-setup.zip 파일을 다운로드 한다.https://github.com/coreybutler/nvm-windows/releases Releases · coreybutler/nvm-windowsA node.js vers

jang8584.tistory.com

 

스타일링은 tailwindCSS로 진행할 예정이기에 tailwindCSS도 설치 및 설정을 해 주었습니다.

pnpm install tailwindcss @tailwindcss/vite @tailwindcss/postcss postcss

 

vite.config.ts에 tailwindcss 추가

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
})

 

postcss.config.mjs 파일을 루츠 폴더에 생성한 후 다음 코드 입력

export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

tailwindCSS는 실제로 postCSS 플러그인을 기반으로 동작하기 때문에 tailwindCSS의 핵심기능인 동적 유틸리티 클래스 생성 및 @tailwind  디렉티브를 사용할려면 필수적으로 함께 설치해야 합니다. 빌드 과정에서 CSS를 처리 함에 있어서 PostCSS가 실행되어 tailwindCSS의 유틸리티 클래스에 대한 CSS 파일을 생성합니다.

 

global.css에 `@import "tailwindcss"`를 입력해 기초적인 설정을 마무리 해줍니다.

 

기초 프로젝트 셋팅은 완료했습니다. 이제 저번 내용에 이어서 기본적인 JSX 사용 방법이나 기초 hook들에 대해서는 알고 있다고 가정하고 React 19에서 바뀌거나 추가된 부분 그리고 기존 hook에 대한 심화적인 내용들에 대해 정리하겠습니다.

 

useState

파생 상태 (+ useMemo를 이용한 최적화)

파생 상태란 기존의 상태(state)를 통해서 얻어낼 수 있는 새로운 상태를 파생 상태라 이야기 합니다. 

import {useState} from 'react';

export default function ExampleComponenet () {
	
    const [userName, setUserName] = useState('')
    
    const userNameDerived = `${userName}입니다.` //파생 상태
    
    reutrn (
    	<div>
        	<input value={useName} onChange={(e) => setUserName(e.target.value)} />
            <p>{}</p>
        </div>
    )
}

- 이러한 파생 상태는 state로 관리하면 안됩니다. 파생 상태를 state로 관리할 경우 원본 상태와 파생 상태가 불일치하는 버그가 발생할 수 있습니다.

- 동일하게 파생 상태를 useEffect 등에서 setState로 관리하면 상태가 바뀔때마다 불필요하게 컴포넌트 리렌더링이 발생되거나 무한 루프가 발생할 수 있습니다.

 

그래서 위 코드와 같이 파생 상태는 const로 정의하여 관리하는 것이 일반적입니다. 또한 이러한 파생 상태는 useMemo를 이용해 최적화도 가능합니다. useMemo를 설명하는 파트에서 더 자세하게 설명 드리겠지만 간단히 설명 드리자면 특정 값(A라 하겠습니다)을 캐싱(메모이제이션)하여, A에 영향을 주는 state가 변경되지 않는다면 컴포넌트가 리렌더링 되는 과정에서 A을 도출하는 연산을 따로하지 않고, 캐싱된 값을 전달할 수 있도록 하는 hook입니다. 이를 기존 파생 상태가 존재하는 코드에 적용한다면 다음과 같습니다.

import {useState} from 'react';

export default function ExampleComponenet () {
	
    const [userName, setUserName] = useState('')
    
    const memoUserNameDerived = userMemo(() => {
    	return `${userName}입니다.`;
    }, [userName]); //userName에 변경되는 경우에만 해당 값 변경
    
    reutrn (
    	<div>
        	<input value={useName} onChange={(e) => setUserName(e.target.value)} />
            <p>{}</p>
        </div>
    )
}

이러한 useMemo는 무분별하게 사용해서는 안되고, 다음과 같은 상황에서 사용하는 것을 추천합니다.

  1. 연산 비용이 큰 계산
  2. 리렌더링이 자주 발생하는 컴포넌트에서 동일한 계산이 반복될 때
  3. props로 객체/배열을 전달하는 과정에서 참조 값이 유지되어야 할 때

3번째 상황에 대해 조금 더 자세한 설명이 필요할 거 같네요.

우선적으로 알아야 할 내용은 React의 값 비교 방식입니다. React는 props나 state 변경에 대해서 데이터의 값이 아닌 참조값(메모리 주소)만 비교하는 얕은 비교(Shallow Comparison)로 판단합니다. <얕은 비교는 숫자나 문자열 boolean과 같은 원시값은 값 자체를 비교하고 객체나 배열은 참조값만 비교합니다. 반대로 깊은 비교는 원시값에 대해서는 값을 동일하게 비교하지만 객체나 배열에 대해서는 구조적으로 모두 일치하는지를 확인합니다.>

 

즉 리렌더링의 여부를 결정하는 과정에 얕은 비교를 사용하는 것이죠 그러므로 props나 state의 참조값이 다르면 내부의 값이 같아도 변경된 것으로 인지하고 리렌더링을 발생 시킵니다.

 

코드로 설명을 드리겠습니다.

function Parent ({value}) {
	return <Child items={[value]} />
}

위와 같이 부모 컴포넌트에서 받은 props를 바로 배열로 변환하여 자식 컴포넌트로 보내준다면, 렌더링마다 새로운 배열을 생성하게 되면서 Child는 매번 리렌더링 되게 됩니다. 이럴 경우

 

fucntion Parent ({value}) {
	const items = useMemo(() => [value], [])
    
    return <Child items={items}>
}

해당 코드처럼 useMemo를 사용하면 자식 컴포넌트가 불필요하게 리렌더링 되는 것을 막을 수 있습니다.

만약 items을 부모가 props로 받은 것이 아니라 useState로 부모에서 선언한 값이라면 setState로 값을 바꾸지 않는 이상 참조값이 바뀌지 않기 때문에 useMemo를 사용하지 않아도 됩니다.

 

 

지연 초기화

useState를 사용하는 과정에서 초기값을 함수를 통해 초기화 하는 경우가 있습니다. 이 과정에서 사용하는 함수가 무거운 연산을 하는 경우 컴포넌트의 성능을 크게 떨어뜨릴 수 있습니다.

const heavyCalculate = () => {
	let sum = 0;
    for (let i = 0; i < 10000000000; i++) {
    	sum += 1
    }
    reutrn sum
}


export default function Component () {
	
    const [state, setState] = useState(heavyCalculate())
    
    
    return (
    	<div>
        	<input value={state} onChange={(e) => setState(e.target.value)} />
        </div>
    )
}

위와 같이 무거운 연산을 하는 함수로 state의 초기값을 설정할 경우 초기값 이후 input에 값을 입력할 때 state에 영향을 주진 않지만 state가 변경되어 리렌더링 될때마다 해당 함수를 실행하며 input 값을 입력하는 과정에 매우 느려지는 것을 확인할 수 있습니다.

 

이런 경우를 대비해 React에서는 지연 초기화 기능(?)을 제공합니다. useState를 선언할 때, 초기값을 콜백 함수로 전달하여 구현한다면, 최초 렌더링 시에만 무거운 연산을 하는 함수를 실행하고, 이후 리렌더링에서는 이전에 계산된 값을 그대로 사용합니다.

 

아래와 같이 사용합니다.

const [state, setState] = useState(() => {
	return heavyCalculate();
})

// or

const [state, setState] = useState(() => heavyCalculate());

// or

const [state, setState] = useState(heavyCalculate);
// 지연 초기화라고 할 순 없지만 위와 같이 사용하면 해당 함수를 초기화 함수로 인식하여
// 지연 초기화와 동일하게 작동합니다.
// 하지만 가독성을 위해 1번 혹은 2번 방법을 추천합니다!

 

그래서 useState의 초기값을 무거운 연산을 하는 함수로 정의한다면 위의 지연 초기화를 이용하는 것이 좋습니다 :)