[React] 상태 관리/ 실습 - fe-sprint-cmarket-hooks
목차
📌 상태 관리
📍상태 관리란?
상태 : UI에 동적으로 표현될 데이터
보라색 글씨 : 상태
빨간 선 : 상태 변경이 일어나는 곳
초록 선 : 상태 변경의 영향을 받는 곳
🔗 프론트엔드 개발에서의 side Effect
함수(또는 컴포넌트)의 입력 외에도 함수의 결과에 영향을 미치는 요인
대표적인 예 : 네트워크 요청(백엔드 API요청)
side Effect을 최대한 배제하고 컴포넌트를 만드세요!!!
fetch와 같은 API 요청이 없어도 컴포넌트는 작동되어야만 한다
어떤 데이터가 들어오는지 상관하지 않고 가짜 데이터라도 컴포넌트는 표현 그 자체에 집중되어야 한다
🔗 React로 사고하기
- UI를 컴포넌트 계층 구조로 나누기 : 모든 컴포넌트 주변에 박스를 그리고 이름을 붙이는 것
- React로 정적인 버전 만들기 : state를 사용하지 않고 동적 버전의 앱을 실제로 구현
- UI state에 대한 최소한의 표현 찾아내기 : 데이터 모델을 변경할 수 있는 방법 -> state
- state가 어디에 있어야 할 지 찾기 : state를 기반으로 렌더링하는 모든 컴포넌트 찾기
- 역방향 데이터 흐름 추가하기 : 예를 들어 콜백을 넘겨서 state가 업데이트 되어야 할 때
🔗상태의 두 가지 구분
로컬 상태 : 컴포넌트 내에서만 영향을 끼치는 상태, 다른 컴포넌트와 데이터를 공유하지 않는 폼데이터는 대부분 로컬 상태이다.
예를 들어 input box, select box, ...
전역 상태 : 다른 컴포넌트와 상태를 공유하고 영향을 끼치는 상태, 전달매체 혹은 데이터 로딩 여부 상태 역시 앱 전반의 영향을 줌
서로 다른 컴포넌트가 동일 한 상태를 다루면 출처는 오직 한 곳이여야 한다 여기서 동일 출처 -> 전역 공간
복사? -> 동기화하는 과정이 필요 허나 복잡하다
💡 전역 상태에서의 데이터 무결성
데이터의 정확성을 보장하기 위해 데이터의 변경이나 수정 시 제한을 두어 안정성을 저해하는 요소를 막고 상태들을 항상 옳게 유지
💡 전역 상태 관리 Case Study
- 예를 들어 라이트모드/다크 모드 테마 처럼 모든 페이지, 컴포넌트에 적용되더야 하기 때문에 이러한 설정은 전역으로 관리
- 국제화(Globalization) 설정 - 사용자 언어
- Undo/Redo를 위한 history 기능
🔗상태 관리를 위한 각종 툴
- React Context
- Redux
- MobX
📍Props Drilling이란?
상위 컴포넌트의 state를 props를 통해 전달하고자 하는 컴포넌트로 전달하기 위해 그 사이는 props를 전달하는 용도로만 쓰이는
컴포넌트들을 거치면서 데이터를 전달하는 현상을 의미
🔗Props Drilling의 문제점
Props의 전달 횟수가 늘어날수록 문제가 생긴다
- 코드의 가독성이 매우 나빠지게 됩니다.
- 코드의 유지보수 또한 힘들어지게 됩니다.
- state 변경시 Props 전달 과정에서 불필요하게 관여된 컴포넌트들 또한 리렌더링이 발생합니다. 따라서, 웹성능에 악영향을 줄 수 있습니다.
🔗 해결 방법
상태관리 라이브러리를 사용하는 방법(Ex : Redux)
컴포넌트와 관련있는 state는 될 수 있으면 가까이에서 보관
📍 fe-sprint-cmarket-hooks
💡point!!!!
상태관리 라이브러리를 사용 하지 않은 웹 페이지를 만들었을 때에서의 구현
Bare minimum requirements
- react-router-dom을 이용해 Client Side Routing하는 방법을 학습합니다.
- useState 를 이용해 상태를 사용하는 방법을 학습합니다.
- 쇼핑몰 애플리케이션의 주요 기능을 구현하세요.
- [장바구니 담기] 버튼을 이용해 장바구니에 해당 상품이 추가되도록 구현하세요.
- 장바구니 내 [삭제] 버튼을 이용해 장바구니의 상품이 제거되도록 구현하세요.
- 장바구니 내에서 각 아이템 개수를 변경할 수 있도록 구현하세요.
- 장바구니의 상품 개수의 변동이 생길 때마다, 상단 내비게이션 바에 상품 개수가 업데이트되도록 구현하세요.
메인 홈
장바구니
구조
<구현>
step01. 가장 상위폴더 App.js에서 [item, setItems] , [carItems, setCartItems] 의 정보 내려주기
app.js
// 아이템의 정보와 카드에 담겨있는 아이템의 정보를 각 컴포넌트의 내려준다
const [items, setItems] = useState(initialState.items);
const [cartItems, setCartItems] = useState(initialState.cartItems);
<Router>
<Nav cartItems={cartItems}/>
<Routes>
// cartItems={cartItems}와 setCartItems={setCartItems}를 props로 넘겨줌
<Route path="/" element={<ItemListContainer items={items} cartItems={cartItems} setCartItems={setCartItems}/>} />
<Route
path="/shoppingcart"
// cartItems={cartItems}와 setCartItems={setCartItems}를 props로 넘겨줌
element={<ShoppingCart cartItems={cartItems} items={items} setCartItems={setCartItems} />}
/>
</Routes>
step02. [장바구니 담기] 버튼을 이용해 장바구니에 해당 상품이 추가되도록 구현하세요.
장바구니 담기 버튼이 어디 컴포넌트에 있는 지 확인 후 -> item.js
item.js
<button className="item-button" onClick={(e) => handleClick(e, item.id)}>장바구니 담기</button>
handleClick 이용하여 item 컴포넌트를 받아 장바구니의 추가하는 형태
ItemListContainer.js
import React from 'react';
import Item from '../components/Item';
function ItemListContainer({ items, cartItems, setCartItems }) {
const handleClick = (e, id) => {
let newCartItem = {};
newCartItem.itemId = id;
newCartItem.quantity = 1;
for(let i = 0; i < cartItems.length; i++){
if(cartItems[i].itemId === id){
setCartItems([...cartItems])
cartItems[i].quantity++
}else{
setCartItems([...cartItems, newCartItem])
}
}
}
return (
<div id="item-list-container">
<div id="item-list-body">
<div id="item-list-title">쓸모없는 선물 모음</div>
{items.map((item, idx) => <Item item={item} key={idx} handleClick={handleClick} />)}
</div>
</div>
);
}
export default ItemListContainer;
step03. 장바구니 내 [삭제] 버튼을 이용해 장바구니의 상품이 제거되도록 구현하세요.
삭제 버튼이 어딨는지 확인 후 -> cartItem.js
cartItem.js
<button className="cart-item-delete" onClick={() => { handleDelete(item.id) }}>삭제</button>
handleDelete를 이용해 해당 컴포넌트를 삭제해주는 명령어를 추가
shoppingCart.js
const handleDelete = (itemId) => {
// 해당 부분의 체크 버튼을 없애주는 코드
setCheckedItems(checkedItems.filter((el) => el !== itemId))
// 위의 코드처럼 filter함수를 이용해 해당 아이템의 아이템 아이디값을 비교해 같은 값만 삭제
// 이때 해당 컴포넌트에는 setCartItems가 없으니 해당 상위컴포넌트에서 받아오기
setCartItems(cartItems.filter((el) => el.itemId !== itemId));
}
step03. 장바구니 내에서 각 아이템 개수를 변경할 수 있도록 구현하세요.
수량 변경 -> handleQuantityChange
shoppingCart.js
// 상품의 아이디, 수량을 받아와서 변경해주면 되므로 두 인자를 받음
const handleQuantityChange = (quantity, itemId) => {
const newCartItems = [...cartItems]
// findIndex()메서드는 주어진 판별 함수를 만족하는 배열의 첫번째 요소에 대한 인덱스를 반환
let findIdx = cartItems.findIndex((item) => item.itemId === itemId)
newCartItems[findIdx].quantity = quantity
setCartItems(newCartItems)
}
step04. 장바구니의 상품 개수의 변동이 생길 때마다, 상단 내비게이션 바에 상품 개수가 업데이트되도록 구현하세요.
app.js에서 nav 부분의 cartItems를 내려준 이유이다
nav.js
import React from 'react';
import { Link } from 'react-router-dom';
function Nav({cartItems}) { // 내려준 인자로 받고
return (
<div id="nav-body">
<span id="title">
<img id="logo" src="../logo.png" alt="logo" />
<span id="name">CMarket</span>
</span>
<div id="menu">
<Link to="/">상품리스트</Link>
<Link to="/shoppingcart">
장바구니<span id="nav-item-counter">{cartItems.length}</span> // item의 길이로 반환
</Link>
</div>
</div>
);
}
export default Nav;
후기
컴포넌트를 구조를 짜야하는 이유가 상위 컴포넌트 하위 컴포넌트를 정확하게 알아야 하는 이유이다
상위 컴포넌트에서 인자를 받고 하위 컴포넌트에서 상태 끌어올리기 또한 가능하다
허나 모든 컴포넌트에서 공통적으로 사용되는 전역변수나 상태관리 시스템 redux를 사용하면
코드가 훨씬 간결해질 수 있다
거의 레퍼런스 코드를 보고 따라친거지만 너무 어렵다,,,, 🤷🏾♀️ 🤷🏾♀️ 🤷🏾♀️