team logo icon
article content thumbnail

리액트에 대해 (상태관리, 렌더링 최적화)

리액트의 중요한 요소 중 하나인 상태 관리, 렌더링 최적화에 대한 아티클.

좋은 상태 관리란 무엇인가?

우선 상태에 대해서 명확히 짚고 넘어가자.

상태 : 컴포넌트 내부에서 관리되며, 어플리케이션의 렌더에 영향을 미치는 요인

💡 상태의 종류

  1. 전역 상태

  2. 컴포넌트 간 상태

  3. 지역 상태


  • 전역 상태 프로젝트 전체에 영향을 끼치는 상태. ex) 유저 기능

  • 컴포넌트 간 상태 여러가지 컴포넌트에서 동시에 관리되는 상태이다. 다수의 컴포넌트에서 쓰이고, 영향을 미치는 상태를 의미한다. ex) 프로젝트 여러 곳에서 쓰이는 모달

  • 지역 상태 특정 컴포넌트 안에서만 관리되는 상태이다. ex) 사용자의 입력을 받아 컴포넌트 렌더링

전역 상태와 컴포넌트 간 상태는 Prop Drilling 방식으로 (부모 → 자식)의 단방향 형태로 데이터를 전달한다는 특징이 있다.

지역 상태는 흔히 사용되는 useState()로 인한 컴포넌트 state를 생각해보면 된다. 그 state는 해당 컴포넌트 내부에서만 사용되기 때문에 지역 상태라고 할 수 있다.

Prop Drilling이란?

단순하게 상위 컴포넌트의 Props를 하위 컴포넌트가 받는 것을 의미하는 것이 아니다!

이 용어의 정확한 뜻은,

“특정 하위 컴포넌트에 필요한 Props가 도달하기까지, 해당 Props가 필요하지 않는 중간 컴포넌트를 거치는 비효율적인 과정”

을 의미한다.

즉, 컴포넌트 간의 종속 관계로 인하여 바로 하위 컴포넌트에 필요한 props를 전달하지 못하고, 상위 컴포넌트로부터 props를 받아와야 하기 때문에 일어나는 문제이다.

이는 나중에 여러가지 단점을 몰고 온다

  • 복잡성 - 필요하지 않은 props를 중간 컴포넌트가 받아와야 하는 문제

  • 유지 보수의 어려움 (중간 컴포넌트에서 이상이 없는지 체크해줘야함)

  • 디버깅의 어려움 (문제가 발생한 곳을 찾기 어려움, Props의 출처를 찾기 어려움)

  • 등등…

이를 해결하기 위해(로컬 상태의 불편함을 개선하기 위해) 상태를 전역 상태로 구현하면, 또다른 문제점(전역이기 때문에 누구나 접근이 가능하다)이 발생할 수 있기 때문에 전역 상태를 이용하되, 특정 컴포넌트들만 접근할 수 있도록 지원해주는 라이브러리가 있다.

그것이 상태 관리 라이브러리이다.

Context API (상태 관리 라이브러리는 아니다.)

설치가 필요한 라이브러리가 아니다! (React 라이브러리에 내장되어 있으며, createContext와 같은 방식으로 사용할 수 있다.)

상태 관리를 위한 기능을 제공하는데, 정확히 말하자면 전역상태관리를 하기 보다는 기존 로컬 상태를 종속성 주입 형태로 변환하여 이벤트 버스를 통해, props drilling을 회피하기 위한 용도로 사용된다고 한다.

실제 상태변경은 useState 및 useReducer 등의 hook 이 진행한다.

  • Context API에 대한 추가 설명

    Context API

    Context API는 React v16.3 이후로 추가된 기능으로, Redux와 같이 전역 상태 관리를 위한 기능을 제공한다. 좀 더 정확히 말하자면, 전역상태관리를 하기 보다는 기존 로컬 상태를 종속성 주입 형태로 변환하여 이벤트 버스를 통해, props drilling을 회피하기 위한 용도로 사용된다. 보통 Context API 를 useState 및 useReducer를 함께 사용하는 편이다. Context API 는 컴포넌트가 상태관리를 공유할 수 있도록 트래킹을 도와주고, 실제 상태변경은 useState 및 useReducer 등의 hook 이 진행한다.

    Context API 장점

    • 따로 설치가 필요없다.

    • 원하는 Provider를 특정 컴포넌트에만 추가하는 것이 가능하다. 추가한 컴포넌트 내에서만 해당 프로바이더에 접근하도록 하는 것이 가능하다.

    • 비교적 낮은 규모의 어플리케이션 상태관리에는 좋다.

    Context API 단점

    • useState 및 useReducer와 함께 사용할 경우, 성능 이슈가 발생할 수 있다.

    • provider 에 속한 컴포넌트의 경우 리렌더링이 발생한다.

    • useMemo, React.momo 를 활용하여 메모이제이션을 통해 리렌더링을 줄일 수 있으나, 작업량이 많아지며 이러한 작업 없이 대체할 수 있는 라이브러리들이 많다.

상태 관리 라이브러리의 종류


redux 라이브러리가 압도적으로 많이 사용된다.

Redux - 가장 많이 사용됨

  • redux에 대한 설명

    Redux

    Redux는 React에서 가장 많이 사용되는 상태관리 라이브러리이다. 원래 자바스크립트 관련 앱의 상태관리를 위해 사용되다가, React 가 등장하며 react-redux 라는 이름을 가지고 react에서 redux를 사용하도록 모듈이 등장하며, 주목을 받게 되었다.

    Redux의 장점

    • 리액트 뿐만 아니라 다른 프레임워크에서도 사용할 수 있다.

    • 성능적인 면에서 Context API 보다 좋다는 사례가 많다.

    • 커뮤니티가 매우 크며, 제공되는 익스텐션이나 툴이 많다.

    • 미들웨어를 추가적으로 구성할 수 있다.

    Redux의 단점

    • 다양한 미들웨어를 제공하는 만큼 러닝커브가 긴 편이다.

    • react-toolkit 을 통해 좀 더 간편하게 작성할 수 있으나, 다른 상태관리 라이브러리 대비 여전히 작업량이 많다

    MobX

    MobX는 Redux와 같은 전역상태관리 라이브러리이면서, React에 비종속적인 라이브러리이다. 객체 지향의 느낌이 강하며, Spring Framework와 유사한 아키텍쳐 구조를 가지고 있어 서버 개발자에게 친숙하다는 평가를 받고 있다.

    MobX 장점

    • Redux 대비 비교적 낮은 러닝커브를 가지고 있다.

    • 다수의 store를 구성할 수 있다.

    MobX 단점

    • devTools, 커뮤니티가 다소 부족하다.

    Context API

    Context API는 React v16.3 이후로 추가된 기능으로, Redux와 같이 전역 상태 관리를 위한 기능을 제공한다. 좀 더 정확히 말하자면, 전역상태관리를 하기 보다는 기존 로컬 상태를 종속성 주입 형태로 변환하여 이벤트 버스를 통해, props drilling을 회피하기 위한 용도로 사용된다. 보통 Context API 를 useState 및 useReducer를 함께 사용하는 편이다. Context API 는 컴포넌트가 상태관리를 공유할 수 있도록 트래킹을 도와주고, 실제 상태변경은 useState 및 useReducer 등의 hook 이 진행한다.

Zustand

Recoil - 최근 부상하고 있으나, 안정성 문제

  • recoli에 대한 설명

    Recoil

    Recoil은 Facebook에서 개발한 React 상태 관리 라이브러리이다. Redux와 유사한 전역 상태 관리 방법을 제공하면서도, Context API와는 달리 상태 변경 시 컴포넌트를 리렌더링하지 않고 필요한 부분만 업데이트한다. 작업량이 Redux, Context API 대비 상대적으로 적으며, 기존의 useState 와 유사하게 사용되기 때문에 개발자가 금방 적응할 수 있다는 것이 장점이다.

    Recoil 장점

    • Redux 대비 store 구성을 위한 코드가 간편하다.

    • 이미 hook을 사용해본 경험이 있는 개발자라면 익숙하게 사용할 수 있다.

    • 공식적으로 typescript를 지원한다.

    Recoil 단점

    • 다른 상태관리 라이브러리보다 등장 시기가 늦어 실제 반영하여 사용하는 회사들이 많지 않다.

    • 다른 라이브러리와의 호환성 문제가 나타날 수 있다는 리스크를 가지고 있다.

MobX

  • MobX에 대한 설명

    MobX

    MobX는 Redux와 같은 전역상태관리 라이브러리이면서, React에 비종속적인 라이브러리이다. 객체 지향의 느낌이 강하며, Spring Framework와 유사한 아키텍쳐 구조를 가지고 있어 서버 개발자에게 친숙하다는 평가를 받고 있다.

    MobX 장점

    • Redux 대비 비교적 낮은 러닝커브를 가지고 있다.

    • 다수의 store를 구성할 수 있다.

    MobX 단점

    • devTools, 커뮤니티가 다소 부족하다.

+React Query

: 전역 상태관리 라이브러리라고 보기보다는, 서버와 클라이언트 간 비동기 작업을 쉽게 다룰 수 있게 도와주는 라이브러리

자세한 내용은, 추후 필요할 때 학습하며 찾아보자.



렌더링 최적화

렌더링을 최적화한다?

: 렌더링 횟수를 최소로 줄인다.

렌더링에는 시간과 자원이 소모된다. 그렇기 때문에, 같은 기능을 하더라도 최대한 리렌더링이 덜 일어나게 설계한다면, 렌더링 최적화를 이루는 것이라고 할 수 있다.

일단 기본적으로 다음의 내용을 짚고 가자.

리액트의 렌더링 과정 : Virtual DOM → 비교 알고리즘(diffing) → Reconciliation(조정)

  1. Virtual DOM 가상 DOM을 하나 생성한다. (JS 객체)

  2. Diffing (비교 알고리즘) 비교 알고리즘에 대해 조금 더 자세히 알아보자. Diffing 알고리즘은, DOM에서 변한 부분이 있으면 반영하기 위한 알고리즘인데, type diff와 key diff 2가지가 존재한다.

  3. Reconciliation(조정) : 최종적으로 리렌더링!

1. Diffing 알고리즘에서 렌더링 최적화

1) type diff : 키 값이 부여되지 않았을 때 비교하는 diff 알고리즘 (비효율적)

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

위의 두 코드에서는 third 요소만 렌더링 큐에 들어가고, 추후 렌더링되면 된다.

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

이 경우에는, diff 알고리즘이 Duke → Connecticut , Villanova → Duke로 변한 뒤에, Villanova가 추가되었다고 판단한다. (사실은 그저 맨 위에 Connecticut가 추가된 것 뿐인데!)

따라서 모든 <li>를 리렌더링하게 된다.

2) key diff: 키 값을 부여하여, DOM에서 필요한 부분만 정확히 리렌더링 diff 알고리즘 (효율적)

이를 보다 효율적으로 개선하기 위해, key diff 알고리즘이 적용되었다. 한줄로 요약하면, 컴포넌트의 인스턴스를 구분하기 위해 key를 부여하는 방식이다.

따라서, key는 고유&불변한 값으로 설정해야한다.

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

key값을 보고, 요소들을 바로 비교하여 필요한 것만 추가/삭제/수정을 진행할 수 있다.

하지만 key값을 부여할 때도 주의해야 할 사항이 있다.

// todoList.length: 10
const TodoList = (todoItems: TodoItem[]) => {
	return (
		<Fragment>
			{todoItems.map((todoItem, index) => <Todo todo={todoItem} key={index}/>}
		</Fragment>
	)
}

보통 리액트 컴포넌트에서는 map을 통해 컴포넌트를 생성 및 key값을 부여하는 일이 잦을텐데, 위의 경우에서 컴포넌트가 중간의 3,4,5번째 아이템 3개가 삭제되고, 4개가 새롭게 추가되면 새로운 key값은 10일 것이다. (기존에는 key는 0~9까지의 값이었다.) 그럼 diff 알고리즘은 key 0~9까지의 컴포넌트는 그대로 있고, 오직 key 10에 해당하는 컴포넌트 하나만 추가되었다고 판단한다.

따라서 똑바로 리렌더링을 일으키려면 반드시 key값이 고유하고, 불변해야만 한다.

2. Reconciliation 에서의 최적화- React.memo 사용

리액트에 존재하는 대표적인 렌더링 최적화 함수이다.

다시 복기하자면, 리액트 리렌더링의 기본 원리 중 하나는 다음과 같다.

“함수형 컴포넌트(부모 컴포넌트)의 State가 변경되면 모든 자식 컴포넌트(return하는 컴포넌트)에 대하여 re-rendering이 일어난다. “

이런 비효율성을 막기 위해 React.memo(함수형컴포넌트);를 사용할 수 있다.

const MemorizedBtn = React.memo(Btn);
  function App()
  {
    const [value, setValue] = React.useState("Save Changes");
    const changeValue = () => setValue("Revert Changes");
    return (
      <div>
        <MemorizedBtn text ={value} onClick={changeValue} />
        <MemorizedBtn text = "Continue"/>
      </div>
    );
  }

이렇게 하면 두번째 <MemorizedBtn/>은 리렌더링을 막을 수 있다. React.memo(Btn);으로 Btn을 기억하는 버전의 컴포넌트를 만들고, 그 컴포넌트는 부모 요소의 State가 변화했다고 하더라도 본인(컴포넌트)의 props가 동일하면 리렌더링을 진행하지 않는다.

(하지만 테스트 중 발견한 특이한 점은, 첫번째 <MemorizedBtn text ={value} onClick={changeValue} />를 <MemorizedBtn text =”fixedexample” onClick={changeValue} /> 로 수정하면 props가 변할 일이 없는데, 리렌더링이 진행된다는 점이다.

이는 아마 onClick={changeValue}에 의한 것 같은데, 그 이유는 명확히 모르겠다 ~

3. 추가적으로, lazy()함수 사용하여 코드 스플릿!

리액트 렌더링 최적화와는 결이 약간 다르지만, 결국 초기 렌더링 측면에서 약간이라도 속도를 증가시키고 싶다면 lazy()함수를 사용하여 필요한 코드만 우선 가져오는 방식을 채택할 수도 있다.

4. 그외 등등..

사실 렌더링 최적화를 이룰 수 있는 방법은 더 있으나 생략했다. 그리고 애초에, 효율적인 렌더링을 위해서는 컴포넌트 설계에서부터 신경 써야 하는 부분들이 많다.

컴포넌트를 적절히 분리하여, 필요없는 컴포넌트까지 (자식이라는 이유로) 리렌더링 되는 상황을 방지하는 것도 렌더링 최적화의 한 부분이라고 할 수 있다.

(컴포넌트)계층분리 & 컴포넌트 설계 & 디자인 패턴 등을 고려하여, 컴포넌트를 제대로 분리하고, 재사용하고, 코드의 가독성을 높여서 렌더링의 효율성을 올릴 수도 있다.


💡 그 중 몇가지 중요한 원칙, 디자인 패턴만 언급하면 다음과 같다.

SOLID 원칙

ex) S(SRP) 를 잘 지키기 위한 컴포넌트를 보는 기준

  1. Headless 기반의 추상화하기

  2. 한가지 역할만 하기

  3. 도메인 분리하기

+ 중복 배제 원칙 (DRY)

React 디자인 패턴

  1. 합성 컴포넌트

  2. 제어 Props 패턴

  3. Custom Hook 패턴

  4. Props Getter 패턴

  5. State Reducer 패턴




상태관리와 관련하여 참고한 사이트)

https://velog.io/@beberiche/React의-상태관리-방법에-대해-설명해주세요

https://yozm.wishket.com/magazine/detail/2233/

https://velog.io/@jutrong/리액트와-상태-관리-라이브러리

https://mingule.tistory.com/74

https://velog.io/@wjdwl002/React-상태관리-라이브러리-비교분석-Redux-Recoil-편

https://velog.io/@velopert/react-context-tutorial - Context 사용법

렌더링 효율성과 관련하여 참고한 사이트)

https://www.nextree.io/riaegteu-rendeoring-mic-coejeoghwa/

https://legacy.reactjs.org/docs/reconciliation.html?ref=nextree.io

https://5kdk.tistory.com/36

https://velog.io/@superlipbalm/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior

https://www.developerway.com/posts/react-re-renders-guide#part2.5

최신 아티클
lighthouse에 대해
문성희
|
2024.05.13
lighthouse에 대해
lighthouse에 대해
prettier, eslint, styleLint에 대해
이진
|
2024.05.10
prettier, eslint, styleLint에 대해
4주차 공유과제
Article Thumbnail
박채연
|
2024.05.10
Prettier, ESLint, StyleLint
prettier, eslint, stylelint