상세 컨텐츠

본문 제목

[React.js 1] 7-8장. useReducer와 상태관리 & 최적화

23-24/React.js 1

by ssxb 2023. 12. 29. 10:00

본문

728x90

useReducer 이해하기

useReducer는 useState와 더불어  리액트 컴포넌트에서 State를 관리하는 리액트 훅입니다. useReducer는 State 관리를 컴포넌트 내부가 아닌 외부에서 할 수 있게 만듭니다.  useState와 달리 useReducer를 이용하면 컴포넌트에서 상태 변화 코드를 쉽게 분리할 수 있습니다.

📌 상태 변화 코드

State값을 변경하는 코드

상태 변화 코드를 분리한다는 말은 컴포넌트 내부에 작성했던 상태 변화 코드를 외부에 작성한다는 뜻입니다.

그러나 useState를 이용해 State를 만들면 상태 변화 코드 분리 불가합니다. 반드시 컴포넌트 안에 작성해주어야 합니다.

하지만 useReducer를 사용하면 상태 변화 코드를 컴포넌트 밖으로 분리할 수 있습니다.

 

[useReducer의 용법]

const [count, dispatch] = useReducer(reducer, 0);

 

count는 state 변수, dispatch는 상태 변화 촉발 함수, useReducer(reducer, 0)는 생성자(상태 변화 함수, 초깃값)

useReducer가 반환하는 함수 dispatch를 호출하면, useReducer는 함수 reduce를 호출하고, 이 함수가 반환하는 값이 State를 업데이트합니다.  즉, useReducer는 함수 reducer를 이용해 상태 변화 코드를 컴포넌트 외부로 분리하는 것입니다. 

 

함수 useReducer로 상태 변화 코드를 컴포넌트와 분리해 할 일 관리 앱을 업그레이드 해보세요! 👀

먼저 useState를 useReducer로 바꾸어 보겠습니다. 실무에서는 컴포넌트를 관리하는 State가 복잡하지 않으면 useState를 사용하고, 그렇지 않으면 useReducer를 사용합니다. 

import { useReducer, useRef } from "react";
(...)
function reducer(state, action) {
  // 상태 변화 코드
  return state;
}

function App() {
  const [todo, dispatch] = useReducer(reducer, mockTodo);
  (...)
}
export default App;

 

여기까지 수정한 위의 파일을 저장하면 호출하던 함수 setTodo 함수가 사라졌기 때문에 오류가 발생할 것입니다.  앞으로 상태 변화가 필요할 때는 set 함수 대신 상태 변화 촉발 함수인 dispatch를 호출해야 합니다. 따라서 더 이상 함수 setTodo는 사용하지 않고, App 컴포넌트에서 함수 setTodo를 호출하는 코드를 모두 제거합니다.

 

그 다음, useReducer로 할 일 아이템 추가 기능을 구현해보겠습니다. 우선 함수 onCreate에서 dispatch를 호출하고, 인수로 할 일 정보를 담은 action 객체를 전달합니다. App 컴포넌트를 아래와 같이 수정해보세요.

(...)
function App() {
  (...)
  const onCreate = (content) => {
    dispatch({
      type: "CREATE",
      newItem: {
        id: idRef.current,
        content,
        isDone: false,
        createdDate: newDate().getTime(),
     },
  });
  idRef.current += 1;
 };
 (...)
}
export default App;

 

이제 함수 reducer에서 action 객체의 type이 CREATE일 때, 새 아이템을 추가하는 상태 변화 코드를 작성합니다

 

(...)
fuction reducer(state, action) {
  switch(action.type) {
    case "CREATE": {
      return [action.newItem, ...state];
    }
    default:
      return state;
  }
}
(...)

 

할 일 아이템을 수정하는 함수 onUpdate와 할 일을 삭제하는 함수 onDelete도 수정해보세요!

 


최적화

웹서비스의 성능을 개선하는 기술 / 프로그래밍에서 불필요하게 낭비되는 연산을 줄여 렌더링의 성능을 높이는 방법

최적화 방법으로는 코드, 폰트, 이미지 파일의 크기를 줄이는 등 여러 기술이 있지만, 이 블로그에서는 최적화의 기본이라고 할 수 있는 '리액트의 연산 낭비'를 줄이는 데 초점을 맞추어 다루겠습니다. 리액트 앱에서 연산 최적화는 대부분 메모이지에션(Memoization) 기법을 이용합니다.  

📌 메모이지에션(Memoization) 

특정 입력에 대한 결과를 계산해 메모리 어딘가에 저장했다가, 동일한 요청이 들어오면 저장한 결괏값을 제공해 빠르게 응답하는 기술

이 기법을 이용하면 불필요한 연산을 줄여 주어 프로그램의 실행 속도를 빠르게  만듭니다.

 

함수의 불필요한 재호출 방지

☑️ useMemo

메모이제이션 기법을 이용해 연산의 결괏값을 기억했다가 필요할 때 사용함으로써 불필요한 함수 호출을 막아주는 리액트 훅. useMemo를 사용하면 특정 함수를 호출했을 때 그 함수의 반환값을 기억합니다. 그리고 같은 함수를 다시 호출하면 기억해 두었던 값을 반환합니다. 즉, 함수의 반환값을 다시 구하는 불필요한 연산을 수행하지 않아 성능을 최적화할 수 있습니다. 

const value = useMemo(callback, deps);

 

함수 useMemo를 호출하고 2개의 인수로 콜백 함수와 의존성 배열(deps)을 전달합니다. 호출된 useMemo는 의존성 배열에 담긴 값이 바뀌면 콜백 함수를 다시 실행하고 결괏값을 반환합니다.  

const value = useMemo(() => {
  return count * count;
}, [count]);

 

이와 같이 useMemo를 사용하면 의존성 배열 count의 값이 변할 때만 count * count를 계산해 value에 저장합니다.

 

불필요한 컴포넌트 리렌더 방지

☑️ React.memo

React.Memo를 이용하면 메모이제이션 기법으로 컴포넌트가 불필요하게 리렌더되는 상황을 방지할 수 있습니다. React.Memo를 이해하기 위해서는 먼저 고차 컴포넌트와 횡단 관심사에 관한 이해가 필요합니다. 

 

📌 고차 컴포넌트

인수로 전달된 컴포넌트를 새로운 컴포넌트로 반환하는 함수

고차 컴포넌트는 전달된 컴포넌트를 그대로 반환하는 게 아니라 어떤 기능을 추가해 반환한다는 것을 기억해야 합니다.

📌 횡단 관심사

고차 컴포넌트를 이용하면 횡단 관심사 문제를 효율적으로 해결할 수 있어 실무에서 많이 활용합니다. 횡단 관심사란 크로스 커팅 관심사라고도 하는데, 프로그래밍에서 비즈니스 로직과 구분되는 공통 기능을 지칭할 때 사용하는 용어입니다. 반면 비즈니스 로직은 해당 컴포넌트가 존재하는 핵심 기능을 표현할 때 사용합니다. 컴포넌트의 핵심 기능(비즈니스 로직)을 세로로 배치한다고 했을 때, 여러 컴포넌트에서 공통으로 사용하는 기능은 가로로 배치하게 됩니다. 따라서 공통 기능들이 핵심 컴포넌트들을 마치 '횡단'하는 모습입니다. 여러 컴포넌트에서 횡단 관심사 코드를 작성하는 일은 중복 코드를 만드는 주된 요인 중 하나입니다. 고차 컴포넌트를 이용하면 횡단 관심사 코드를 함수로 분리할 수 있습니다. 

 

const memoizedComp = React.memo(Comp);

 

React.memo를 사용하는 방법은 매우 간단합니다. 단지 강화하고 싶은, 즉 메모이제이션을 적용하고 싶은 컴포넌트를 React.memo로 감싸면 됩니다. React.memo는 Props의 변경 여부를 기준으로 컴포넌트의 리렌더 여부를 결정합니다.

const Comp = ({ a, b, c }) => {
  console.log("컴포넌트가 호출되었습니다.");
  return <div>CompA</div>;
};

function areEqual(prevProps, nextProps) {
  if (prevProps.a === nextProps.a) {
    return true;
  } else {
    return false;
  }
}

const MemoizedComp = React.memo(Comp, areEqual);

 

만약 Props로 전달되는 값이 많을 때는 위와 같이 판별 함수를 인수로 전달해 Props의 특정 값만으로 리렌더 여부를 판단할 수 있습니다. 

 

불필요한 함수 재생성 방지

☑️ useCallback

컴포넌트가 리렌더될 때 내부에 작성된 함수를 다시 생성하지 않도록 메모이제이션하는 리액트 훅

const memoizedFunc = useCallback(func, deps)

 

useCallback은 useMemo처럼 2개의 인수를 전달합니다. 첫 번째 인수로는 메모이제이션하려는 콜백 함수를 전달하고, 두 번째 인수로는 의존성 배열을 전달합니다. 결과로 useCallback은 의존성 배열에 담긴 값이 바뀌면 첫 번째 인수로 전달한 콜백 함수를 다시 만들어 전달합니다. 만약 첫 번째 인수로 전달한 콜백 함수를 어떤 경우에도 다시 생성되지 않게 하려면 의존성 배열을 빈 배열로 전달하면 됩니다. 

 

최적화할 때 유의할 점

1. 최적화는 항상 마지막에 하세요

 

2. 모든 것을 최적화할 필요는 없습니다.

최적화는 일반적으로 부화가 많으리라 예상되거나, 복잡하고 비싼 연산을 수행하거나, 리스트처럼 컴포넌트가 반복적으로 나타날 것이 예상되는 지점을 대상으로 진행합니다.

 

3. 컴포넌트 구조를 잘 설계했는지 다시 한번 돌아보세요

하나의 컴포넌트에 많은 State를 생성하는 것은 매우 비효율적이며 최적화하기도 어렵습니다. 따라서 컴포넌트를 기능이나 역할 단위로 잘 분리했는지 먼저 확인한 다음 최적화하는 것이 바람직합니다.

 

 


 

Quiz

1. ( useReducer )는 useState와 더불어 리액트 컴포넌트에서 State를 관리하는 리액트 훅으로, State 관리를 컴포넌트 내부가 아닌 외부에서 할 수 있게 만든다.

2. 웹서비스의 성능을 개선하는 기술이면서 프로그래밍에서 불필요하게 낭비되는 연산을 줄여 렌더링의 성능을 높이는 방법을 ( 최적화 )라고 한다. 

3. ( 메모이지에션 )은 특정 입력에 대한 결과를 계산해 메모리 어딘가에 저장했다가, 동일한 요청이 들어오면 저장한 결괏값을 제공해 빠르게 응답하는 기술이다. 이 기법을 이용하면 불필요한 연산을 줄여 주어 프로그램의 실행 속도를 빠르게 만들 수 있다.

4. 연산의 결괏값을 기억했다가 필요할 때 사용함으로써 불필요한 함수 호출을 막아주는 리액트 훅인 ( useMemo )를 사용하면 함수의 반환값을 다시 구하는 불필요한 연산을 수행하지 않아 성능을 최적화할 수 있다. 

5. ( 고차 컴포넌트 )는 인수로 전달된 컴포넌트를 새로운 컴포넌트로 반환하는 함수로,  전달된 컴포넌트를 그대로 반환하지 않고 어떤 기능을 추가해 반환한다.

6. ( useCallback )은 컴포넌트가 리렌더될 때 내부에 작성된 함수를 다시 생성하지 않도록 메모이제이션하는 리액트 훅이다. 

7. 최적화는 항상 ( 마지막 )에 해야 한다.

8. 주석에 들어갈 코드를 작성해보세요. action.type이 DELETE일 때 수행할 상태 변화 코드를 작성하면 됩니다.

(...)
function reducer(state, action) {
  switch (action.type) {
    (...)
    // filter 메서드로 id와 targetId가 일치하는 할 일 아이템만 제외한 할 일 배열 생성해 반환
    default:
      return state;
  }
}
(...)

 

정답

(...)
function reducer(state, action) {
  switch (action.type) {
    (...)
    case "DELETE": {
      return state.filter((it) => it.id !== action.targetId);
    }
    default:
      return state;
  }
}
(...)

 

9.  주석에 들어갈 코드를 작성해보세요. 

(...)
function reducer(state, action) {
  // switch문에서 action 객체의 type별로 다른 상태 변화 코드 수행
      // action 객체의 type이 CREATE일 때 동작할 case문 작성
        // action 객체의 newItem에는 추가할 아이템이 저장되어 있음. 기존 할 일 아이템에 action 객체의 아이템이 추가된 새 배열 반환
      }
      default:
        return state;
   }
 }
 (...)

 

정답

(...)
function reducer(state, action) {
  switch (action.type) {
    case "CREATE" :
      return [action.newItem, ...state];
    }
    default:
      return state;
  }
}
 (...)

 

 


 

해당 포스트는 한 입 크기로 잘라먹는 리액트(이정환 저) 책을 참고했습니다. 

Corner React.js 1

Editor: nini

 

728x90

관련글 더보기