Unit4 - [React] 상태 관리

강성일·2023년 6월 22일
0
post-thumbnail

✅ TIL


이번 유닛은 프론트엔드 개발에서 상태 관리를 보다 더 효율적으로 할 수 있는 방법에 대해서 학습한다.

바로 컴포넌트와 상태를 분리하여 전역에서 상태 관리를 해줄 수 있게 해주는 상태 관리 라이브러리인 Redux이다.

React 애플리케이션을 개발할 때 Redux를 사용하면 React 컴포넌트 간의 복잡한 데이터 흐름을 따라갈 필요가 없어진다.
특히 컴포넌트가 많아지고 애플리케이션의 구조가 고도화될수록 Redux를 활용한 상태 관리는 빛을 발한다.

먼저 오늘은 상태 관리 라이브러리인 Redux를 알아보기 전에 프론트엔드 개발에서의
상태 관리에 대한 개념과 상태 관리 라이브러리의 필요성에 대해서 먼저 알아보도록 하겠다.



상태 관리


전역 상태 관리

먼저 상태란? 변하는 데이터이다.

특별히 UI, 프론트엔드 개발에서는 "동적으로 표현되는 데이터"이다.

여기에 쇼핑몰에서 흔하게 볼 수 있는 장바구니 화면이 있다. 여기에는 몇 개의 상태가 있을까?

  • "장바구니에 담기"와 같은 버튼을 눌러, 해당 물품을 장바구니에 추가할 수 있다.
    동적인 데이터이므로 이것은 상태이다.
  • 상단에 [일반구매/정기배송]중 현재 선택된 탭이 무엇인지 나타내는 상태가 있을 수 있다.
  • 상품 선택 여부에 따라 주문 금액이나 배송비가 달라진다. 선택 여부는 변할 수 있으므로 상태이다.
  • 상품 수량도 상태이다.

React의 주요 개발 원칙 중 하나는 UI를 페이지 단위가 아닌 컴포넌트 단위로 보는 것이다.

만일 그림과 같이 <CartItem>이라는 컴포넌트를 만든다면,
fetch와 같은 API 요청이 없이도 이 컴포넌트는 작동되어야 한다.

즉, 'Side Effect'를 배제하고 만들어야 한다.

✨ Side Effect: 최대한 함수의 입력 외에도 함수의 결과에 영향을 미치는 요인

어떤 데이터가 들어오는지 상관하지 않고, 설사 가짜 데이터라도 컴포넌트는 표현(presentation)에 집중해야 한다.

상태를 구분하는 데에는 절대적인 기준이나 법칙이 있는 것은 아니지만,
처음 JavaScript를 배울 때처럼 로컬 상태, 전역 상태로 나눠서 접근해 보겠다.

로컬 상태는 특정 컴포넌트 안에서만 관리되는 상태이며,
전역 상태는 프로덕트 전체 혹은 여러 가지 컴포넌트가 동시에 관리하는 상태를 말한다.

로컬 상태를 구분하는 것은 쉽다. 보통 컴포넌트 내에서만 영향을 끼치는 상태는 로컬 상태이다.

<CartItem> 컴포넌트의 경우, '선택한 수량'이 되겠다.
원래 가격에 상태를 곱해 컴포넌트 내에 표시되는 주문 금액을 업데이트하면 된다.

다른 컴포넌트와 데이터를 공유하지 않는 폼(form) 데이터는 대부분 로컬 상태이다.
input box, select box 등과 같이 입력값을 받는 경우가 이에 해당한다.

전역 상태는 다른 컴포넌트와 상태를 공유하고 영향을 끼치는 상태이다.

장바구니에 담긴 물품의 경우, 상품 선택 여부에 따라 총 주문 금액을 업데이트해야 한다.
장바구니에 담긴 물품은 그 갯수 등을 다른 컴포넌트에 전달해 주어야 한다.

아까 언급한 데이터 로딩 여부(로딩 중) 상태 역시, 앱 전반에 영향을 미친다.

JavaScript를 처음 배우면 전역 변수를 남용하는 것은 좋지 않다고 배웠으나, 경우에 따라 전역 상태가 필요하다.

서로 다른 컴포넌트가 사용하는 상태의 종류가 다르면, 꼭 전역 상태일 필요는 없다. 출처(source)가 달라도 된다.

그러나, 서로 다른 컴포넌트가 동일한 상태를 다룬다면, 이 출처는 오직 한 곳이어야 한다.

만일 사본이 있을 경우, 두 데이터는 서로 동기화(sync)하는 과정이 필요한데, 이는 문제를 어렵게 만든다.
한 곳에서만 상태를 저장하고 접근하자. 여기서 '하나의 출처'는 다른 말로 이야기하면 '전역 공간'이라고 볼 수 있다.

그렇다면 전역으로 상태를 관리해야 하는 경우가 어떤 것이 있을까?
큰 예시로는 다크 모드 기능, 국제화(Globalization) 설정 등이 있다.

이 상태 관리를 도와주는 각종 툴이 있다.

상태 관리 라이브러리가 이러한 문제를 해결해준다.

  • 앞서 꾸준히 언급한 전역 상태를 위한 저장소를 제공한다.
  • props drilling 문제를 해결한다.


Props Drilling

Props Drilling은 상위 컴포넌트의 state를 props를 통해 전달하고자 하는 컴포넌트로 전달하기 위해
그 사이는 props를 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 데이터를 전달하는 현상을 의미한다.

위 그림처럼 컴포넌트 A의 state를 컴포넌트 D로 전달하기 위해선 사이에 있는 컴포넌트 B, C를 거쳐야 한다.

Props Drilling의 문제점

Props의 전달 횟수가 5회 이내로 많지 않다면 Props Drilling 은 큰 문제가 되지 않는다.
하지만 규모가 커지고 구조가 복잡해지면서 Props의 전달 과정이 늘어난다면 아래와 같은 문제가 발생한다.

  • 코드의 가독성이 매우 나빠지게 된다.
  • 코드의 유지보수 또한 힘들어지게 된다.
  • state 변경 시 Props 전달 과정에서 불필요하게 관여된 컴포넌트들 또한 리렌더링이 발생한다.
    따라서, 웹성능에 악영향을 줄 수 있다.

해결 방법

과도한 Props Drilling을 방지하기 위한 방법으로는 컴포넌트와 관련 있는
state는 될 수 있으면 가까이 유지하는 방법과 상태관리 라이브러리를 사용하는 방법이 있다.

상태관리 라이브러리를 사용하게 되면 전역으로 관리하는 저장소에서
직접 state를 꺼내쓸 수 있기 때문에 Props Drilling을 방지하기에 매우 효과적이다.

이번 유닛에서는 다양한 상태관리 라이브러리(Redux, Context api, Mobx, Recoil 등) 중 Redux를 다룰 예정이다.



Props Drilling 예시



위의 예시는 props를 제일 깊은 컴포넌트까지 내려주어야 제대로 작동한다. 코드를 보고 직접 props를 내려보자.

지금도 코드를 작성하는 게 번거롭고 복잡한데 이보다 더 깊은 컴포넌트와 복잡한 구조가 있다고 생각해 보자.

매우.. 끔찍할 것이다.



위 예시는 Child6에 있는 👋 버튼을 누르면 Child3에 느낌표가 하나씩 추가되는 간단한 애플리케이션이다.

이때, Child3, Child6이 하나의 상태를 공유하기 때문에 최상위 컴포넌트인 App에서 상태를 관리해야 한다.
이 때문에 상태를 변경할 때마다 App 컴포넌트가 리렌더링 되면서 모든 컴포넌트가 리렌더링 된다.

변경되는 상태와 연관이 없는 컴포넌트까지 불필요하게 리렌더링 되는 것이다.
아래 예시에서 콘솔창을 열고 👋 버튼을 눌러 리렌더링 되는 컴포넌트를 확인할 수 있다.



💬 Sprint Review


Cmarket (Hooks 버전)



  • react-router-dom을 이용해 Client Side Routing하는 방법을 학습합니다.
  • useState를 이용해 상태를 사용하는 방법을 학습합니다.
  • 쇼핑몰 애플리케이션의 주요 기능을 구현하세요.
    • [장바구니 담기] 버튼을 이용해 장바구니에 해당 상품이 추가되도록 구현하세요.
    • 장바구니 내 [삭제] 버튼을 이용해 장바구니의 상품이 제거되도록 구현하세요.
    • 장바구니 내에서 각 아이템 개수를 변경할 수 있도록 구현하세요.
    • 장바구니의 상품 개수의 변동이 생길 때마다, 상단 내비게이션 바에 상품 개수가 업데이트되도록 구현하세요.


💬 Review


페이지 전환 & 네비게이션 바



  • <App> 루트 컴포넌트

    • <Router>, <Routes>, <Route>로 구분을 지어준다.
    • Nav 컴포넌트에 props를 전달한다.
    • ItemListContainer, ShoppingCart 컴포넌트에 상태 관리 중인 props를 전달한다.
  • <Nav> 내비게이션 바

    • SPA 내에서 화면 전환에 따라 URL를 업데이트하기 위해 <Link> 컴포넌트를 사용한다.
    • { cartItems } 로 props를 받아와서 장바구니 spancartItems.length
      장바구니 담기 버튼 클릭 시, 상품 개수가 즉시 표시되게 한다.


장바구니에 추가 및 상품 개수 업데이트



  • <ItemListContainer> - 메인 페이지 컴포넌트

    🗓️ ToDo. 메인 화면에서 [장바구니 담기] 버튼을 누른 후, 장바구니 페이지로 이동하면 상품이 담겨있어야 한다.

    • 장바구니에 이미 있는 아이템인지, 아닌지에 대한 확인 & 추가 작업을 한다

    • findIndex 메서드를 사용하여 itemId 속성이 id(클릭된 상품의 고유 식별자)와 일치하는 요소를 찾는다.

      • findIndex 함수는 배열에서 주어진 조건을 만족하는 첫 번째 요소의 인덱스를 반환하고,
        조건을 만족하는 요소가 없으면 -1을 반환한다.
    • findIndex의 결과를 findNumber 변수에 저장한다.

    • findNumber가 -1인 경우, 선택한 상품이 장바구니에 없는 상품이므로, setCartItems 함수를 사용하여 새로운 아이템을 장바구니에 추가한다. setCartItems 함수는 이전 장바구니 아이템 목록(cartItems)에 새로운 아이템을 추가하는 방식으로 업데이트한다. 새로운 아이템은 { itemId: id, quantity: 1 } 형식으로 구성되며, itemId는 상품의 고유 식별자이고 quantity는 수량을 나타낸다.

    • findNumber가 -1이 아닌 경우, 선택한 상품이 이미 장바구니에 있는 상품이므로, 해당 상품의 수량을 1 증가시킨다. 그리고 setCartItems 함수를 사용하여 장바구니 아이템 목록을 업데이트한다. 이때, 업데이트할 때는 cartItems 배열을 복사하여 사용한다.


  • <ShoppingCart> - 장바구니 페이지 컴포넌트

    🗓️ ToDo. 장바구니 페이지에서 장바구니에 담긴 각 아이템의 개수를 변경할 수 있어야 한다.

    • 위 ToDo에 해당하는 기능은 컴포넌트 안에서 handleQuantityChange 함수가 담당한다.
    • 함수는 두 개의 매개변수인 quantityitemId를 받는다.
    • cartItems 배열의 map 함수를 사용하여 각각의 장바구니 아이템에 대해 처리한다.
      map 함수는 배열의 각 요소를 변환하여 새로운 배열을 생성한다.
    • map 함수 내부에서는 현재 처리 중인 장바구니 아이템(el)의 itemId와 입력된 itemId를 비교한다.
      • 만약 두 값이 일치한다면 해당 아이템의 수량을 입력된 quantity
        변경한 객체 { itemId: itemId, quantity: quantity }를 반환한다.
      • 두 값이 일치하지 않는 경우에는 현재 장바구니 아이템(el)을 그대로 반환한다.
        이는 map 함수가 해당 아이템을 변경하지 않고 그대로 유지하도록 하는 역할을 한다.
    • map 함수의 결과로 생성된 새로운 배열은 setCartItems 함수를 사용하여 장바구니 아이템 목록을 업데이트한다. 이를 통해 특정 상품의 수량이 변경된 장바구니 아이템 목록으로 상태가 업데이트된다.


장바구니로부터 제거



  • <ShoppingCart> - 장바구니 페이지 컴포넌트

    🗓️ ToDo. 장바구니 페이지에서 [삭제] 버튼을 누른 후, 해당 상품이 목록에서 삭제되어야 한다.

    • 위 ToDo에 해당하는 기능은 컴포넌트 안에서 handleDelete 함수가 담당한다.
    • 함수는 itemId라는 매개변수를 받는다.
    • setCheckedItems 함수를 사용하여 선택된 상품 목록(checkedItems)을 업데이트한다.
      • checkedItems 배열의 filter 함수를 사용하여 선택된 상품 목록에서 itemId와 일치하지 않는 상품들로 이루어진 새로운 배열을 생성한다. 이를 통해 itemId와 일치하는 상품은 삭제된 상태로 업데이트된다.
    • setCartItems 함수를 사용하여 장바구니 아이템 목록(cartItems)을 업데이트한다.
      • cartItems 배열의 filter 함수를 사용하여 itemId와 일치하지 않는 장바구니 아이템들로 이루어진 새로운 배열을 생성한다. 이를 통해 itemId와 일치하는 상품은 장바구니에서 삭제되었으며, 업데이트된 장바구니 아이템 목록으로 상태가 업데이트된다.
profile
아이디어가 넘치는 프론트엔드를 꿈꿉니다 🔥

0개의 댓글