React 상태 관리

✨ 강은비·2022년 2월 6일
0

React

목록 보기
31/36
post-thumbnail

📌 React의 상태 관리

⚛️ 상태(state)란?

Plain Javascript object holds information that influences the output of render

React 공식문서에서는 위와 같이 상태를 정의했다. state는 컴포넌트를 렌더링하는데 있어 영향을 주는 정보를 지닌 객체로, 컴포넌트 내부에서 관리된다.
애플리케이션에서 보여지는 데이터 혹은 UI/UX(FE)에 필요한 데이터 중 시간에 따라 변할 가능성이 있는 데이터를 state에 담아 관리한다.


⚛️ 상태 관리 필요성

하나의 컴포넌트에서 상태를 관리하는 것은 매우 간단하다. 하지만 여러 컴포넌트에서 같은 상태를 공통적으로 접근하고 공유해야 할 때 효율적인 상태 관리가 필요하다.


⚛️ 상태 끌어올리기

Lifting State Up

여러 컴포넌트에서 공유해야 할 상태가 있다면 공통 소유 컴포넌트를 찾아 해당 컴포넌트의 state로 관리한다. 만약 특정 컴포넌트에서 state가 필요하다면 props로 전달한다.

  • 하지만 컴포넌트의 구조가 깊어질수록 props로 상태를 전달하는 것이 비효율적이다.
  • 또한, 해당 상태를 사용하지 않은 컴포넌트에도 props로 상태를 전달해야 하는 상황이 발생한다.

이 문제를 해결할 수 있는 방법들을 리액트에서 제공한다.

공통 소유 컴포넌트란? common owner component
계층 구조 내에서 특정 state가 있어야 하는 모든 컴포넌트들의 상위에 있는 하나의 컴포넌트

⚛️ 컴포넌트 합성

먼저, 컴포넌트 합성 (Composite)를 이용하는 방식이다.

  • 컴포넌트 합성은 props로 전달될 수 있는 값에 대해 제한이 없다는 것을 이용한 것이다.
  • 앞서 상태 끌어올리기의 문제점은 계속적으로 상태를 props로 전달하는 것이다. 하지만 합성에서는 props로 상태를 전달하는 것이 아니라 아예 컴포넌트를 전달한다.
  • 컴포넌트 합성 역시 컴포넌트 계층 구조가 복잡해질수록 상태 관리하기에 비효율적인 방법이 될 수 있다.
function App() {
  const [state, setState] = useState('state');
  
  return (
    <>
      <Header state={state} setState={setState} />
      <Navbar state={state} setState={setState} />
    </>
  );
}
function Header({state, setState}) {
  return (
    <>
       // 또 다시 props로 상태와 setter함수 전달
      <Logo state={state} />
      <Setting setState={setState} />
    </>
  );
}
// 합성 이용
function App() {
  const [state, setState] = useState('state');
  
  return (
    <>
      <Header 
        logo={<Logo state={state} />}
        setting={<Setting setState={setState} />}
      />
      ...
    </>
  );
}  

⚛️ Context API

다른 방법으로 리액트는 Context API를 제공한다. Context API는 주로 전역 상태를 관리할 때 사용되며 컴포넌트끼리 상태를 props를 전달하지 않고도 한 번에 원하는 컴포넌트로 상태를 가져올 수 있다. 하지만 성능면에서 문제점이 발생한다.

  • 유명한 상태 관리 라이브러리인 Redux와 비교하자면 Redux는 상태의 특정 값을 컴포넌트에서 의존하게 될 때 해당 값이 바뀔 때에만 리렌더링이 되도록 최적화되어 있다.
  • 반면, Context에서는 컴포넌트가 상태의 특정 값에 의존할 경우, 해당 값 말고 다른 값이 변경될 때도 컴포넌트가 리렌더링된다.

그래서 관심사에 맞게 Context를 분리하여 생성하는 것이 중요하다. 서로 관련 없는 상태라면 같은 Context에 있으면 안된다.

import React, { createContext, useState, useContext } from "react";

const UserContext = createContext(null);

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

function useUser() {
  return useContext(UserContext);
}

function UserInfo() {
  const { user } = useUser();
  if (!user) return <div>사용자 정보가 없습니다.</div>;
  return <div>{user.username}</div>;
}

function Authenticate() {
  const { setUser } = useUser();
  const onClick = () => {
    setUser({ username: "velopert" });
  };
  return <button onClick={onClick}>사용자 인증</button>;
}

export default function App() {
  return (
    <UserProvider>
      <UserInfo />
      <Authenticate />
    </UserProvider>
  );
}

사용자가 사용자 인증 버튼을 누르면 Context의 user 상태가 업데이트되므로 UserInfo 컴포넌트는 당연히 리렌더링이 된다. 하지만 user 상태에 의존하지 않는 Authenticate 컴포넌트도 리렌더링되는 문제가 발생한다. 왜냐하면 Authenticate 컴포넌트가 의존하고 있는 setUseruser가 같은 Context에 있기 때문이다. 이러한 소규모 애플리케이션의 경우 큰 문제가 되지 않지만 애플리케이션 규모가 커질수록 성능 문제가 발생할 수 있다.

따라서, 위 상황에서 두 개의 Context를 사용해야 한다.

import React, { createContext, useState, useContext } from "react";

const UserContext = createContext(null);
const UserUpdateContext = createContext(null);

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  return (
    <UserContext.Provider value={user}>
      <UserUpdateContext.Provider value={setUser}>
        {children}
      </UserUpdateContext.Provider>
    </UserContext.Provider>
  );
}

function useUser() {
  return useContext(UserContext);
}

function useUserUpdate() {
  return useContext(UserUpdateContext);
}

function UserInfo() {
  const user = useUser();
  if (!user) return <div>사용자 정보가 없습니다.</div>;
  return <div>{user.username}</div>;
}

function Authenticate() {
  const setUser = useUserUpdate();
  const onClick = () => {
    setUser({ username: "velopert" });
  };
  return <button onClick={onClick}>사용자 인증</button>;
}

export default function App() {
  return (
    <UserProvider>
      <UserInfo />
      <Authenticate />
    </UserProvider>
  );
}

user 상태가 업데이트되었을 경우 UserInfo 컴포넌트만 리렌더링된다. 이렇게 업데이트용과 상태용 Context를 분리하는 것이 중요하다.


✨ Redux

💜 Redux Toolkit

Redux Toolkit 라이브러리를 사용하면 액션 타입, 액션 생성함수, 초기 상태, 리듀서를 하나의 함수로 편하게 선언할 수 있다.

  • 이 라이브러리에서는 4가지를 통틀어 slice라고 한다.
  • 이 라이브러리를 사용하면 리듀서를 액션 생성 함수를 한 번에 만들 수 있다.
  • immer가 내장되어 있기 때문에 스프레드 연산자를 사용하지 않고 원하는 값을 직접 변경해도 상태의 불변성이 유지되면서 업데이트된다.
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

export interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1  // 직접 변경 가능
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
})

// 액션 생성 함수
export const { increment, decrement, incrementByAmount } = counterSlice.actions

// 리듀서
export default counterSlice.reducer

💜 Selector

Selector는 다음과 같은 상황에서 유용하게 사용된다.

상태의 위치 변경

만약 상태의 위치가 변경되면 그에 따라 useSelectorselector 함수 인자도 일일이 바꿔야 한다. 하지만 기존에 useSelectorselsector 함수를 따로 선언하고 필요할 때 불러와 사용하면 이후 상태의 위치가 바뀌어도 selector 함수만 변경하면 된다.

컴포넌트 리렌더링 최적화

컴포넌트 리렌더링 최적화를 위해 createSelector를 통해 Memozied Selector를 만들어 사용한다. createSelector에는 여러 selector 함수들이 인자로 전달될 수 있다. 만약 첫 번째 selector에서 반환된 값이 변경될 때에만 그 다음 selector를 호출하여 원하는 값을 연산하여 조회한다.

import { createSelector } from '@reduxjs/toolkit'

const todosSelector = (state) => state.todos;
const undoneTodos = createSelector(
  todosSelector, 
  (todos) => todos.filter((todo) => !todo.done)
);

function UndoneTasks() { 
   const tasks = useSelector(undoneTodos);
   // ...
}

위의 상황에서는 useMemo를 이용하여 최적화할 수 있다.

function UndoneTasks() { 
   const tasks = useSelector(undoneTodos);
   const undoneTasks = useMemo(() => tasks.filter(tasks => !tasks.done), [tasks]);
   // ...
}

💜 Custom Hook

Hook이 도입되기 전, 리덕스를 사용할 때 컴포넌트를 Presentational ComponentContainer Component로 구분하여 작성했다.

  • Presentational Component: props로 상태를 받아와 온전히 뷰만 담당하는 컴포넌트
  • Container Component: 리덕스와 연동되어 있는 컴포넌트로, 상태 업데이트 로직이 존재한다.

이제는 Hook을 통해서 상태 관련 로직을 컴포넌트에서 분리시킬 수 있기에 더 이상 컴포넌트의 구분이 불필요해졌다. 리덕스의 상태와 액션을 사용하여 상태 로직을 Custom Hook에서 작성하면 된다. 컴포넌트에서는 Custom Hook을 이용하여 UI에만 집중하면 된다.


💜 Middleware

미들웨어는 다음과 같은 상황에 유용하게 사용된다.
1. 요청을 연달아서 여러번 하게 될 때 이전 요청은 무시하도록 하고 맨 마지막의 요청만 처리하도록 할 때 (Ex. react-saga의 takeLastest)
2. 특정 조건이 만족되었을 때 이전에 시작한 요청을 취소하는 경우
3. 특정 콜백 함수를 원하는 액션이 디스패치 되었을 때 호출하도록 등록할 때


참고

0개의 댓글