이번 강의의 목표는
여기 있는 '장바구니에 담기' 버튼을 작동시키고
프론트엔드 너머에 있는
장바구니 요소를 제어하되
헤더에 있는 장바구니 버튼을 누르면 그 내용을 볼 수 있게 하는 것입니다
이 장바구니 데이터는
이 어플리케이션 내에 있는 다양한 위치에서
필요한 데이터입니다
예를 들면 각 메뉴 아이템에 있는
버튼을 클릭함으로써 수정할 수 있어야 하고
아직 만들지 않은 새로운 컴포넌트에서
해당 내용을 확인할 수 있어야 합니다
즉 여기 있는 버튼을 클릭하면 볼 수 있어야 합니다
또 장바구니 데이터의 정보를
이 헤더에 있는 버튼에서도 품목 숫자로 확인해야 하고
나중에 결제창 페이지를 만들 때에도
해당 데이터 내용이 필요합니다
다른 말로 하면
이 장바구니 데이터는
한 컴포넌트에서 제어하는 내용이 아니고
만약에 한 컴포넌트에서 제어해야 한다면
그 컴포넌트는 바로 앱 컴포넌트가 되어야 할 것입니다
이 컴포넌트가 헤더를 포함하여
Meals 컴포넌트나 MealsItem 컴포넌트에도 접근할 수 있기 때문입니다
하지만 장바구니 데이터를 앱 컴포넌트에서 제어한다면
일어날 수밖에 없는 결과가 있습니다
바로 이 컴포넌트가 비대화되는 것입니다
여기에 아주 많은 코드를 넣어야 할 것이고
앱에 프롭 드릴링(prop drilling) 작업을 다수 실행하여
장바구니 데이터를 필요로 하는 트리(tree)의 컴포넌트로 보내야 합니다
일반적으로 코드 비대화는 최대한 피해야 하므로
대신 리액트의 컨텍스트(context) 기능을 사용하여
장바구니 데이터를 좀더 일반적인 중앙 집중적 방식으로 제어하겠습니다
먼저는 폴더를 하나 만들겠습니다
위치는 소스 폴더(src)이고 이름은 저장(store)입니다
이전 강의에서도 그랬듯이
이름은 임의로 지으셔도 되지만
보통은 컨텍스트 제어에는 store라는 이름을 씁니다
이 폴더에 CartContext.jsx을 만들어
장바구니 데이터 및 컨텍스트를 여기서 제어하겠습니다
이미 배우셨듯이, 리액트가 제공하는 컨텍스트 기능은
필요로 하는 모든 컴포넌트에
간단하고 재사용 가능한 방식으로
데이터를 펼치게 해 줍니다
이제 이 파일에
리액트에서 createContext를 가져오고
이제 createContext를 호출해서
이 CartContext에 대해 새 컨텍스트 객체를 생성하겠습니다
이제 기본값을 이 컨텍스트로 보낼 수 있습니다
이전에 이미 배우셨듯이
이 강의에서는 이 값이 그다지 필요하지 않습니다
어차피 다른 방식으로 설정하게 될 것이기 때문입니다
하지만 그럼에도 여기에 설정하는 것은
자동 완성에 도움이 되기 때문입니다
또 이 컨텍스트 객체를 준비하는 데에도 도움이 됩니다
여기에서 할 작업은
cartContext가 장바구니 항목 목록을 제어하도록 하고
즉 배열 안에 있는 아이템을 제어하고
함수나 메소드를 객체 안에 추가해서
장바구니의 항목을 더하거나 삭제하도록 하는 것입니다
이 작업을 위해서
addItem 함수를 사용해서
장바구니에 추가된 item, 즉 메뉴 항목을 받고
여기에서 장바구니 내용을 업데이트하겠습니다
물론 이 로직은 여기 있는
임시 플레이스홀더(placeholder) 함수로 들어가지 않고
나중에 다른 함수로 들어가게 됩니다
또 removeItem 함수가 필요합니다
여기에서는 메뉴의 id를 받고
아이템 배열로부터 메뉴 항목을
지울 수 있습니다
이것이 cartContext의 구조가 되겠습니다
이전 강의에서 이미 배우셨듯이
다음 단계로는 일반적으로
컨텍스트를 제공하는 ContextProvider 컴포넌트를 정의합니다
보통은 컴포넌트를 감싸는 형태로 만들어서
이 컨텍스트를 제공합니다
여기에서 데이터 관리를 포함하여
실제 상태 관리가 이루어집니다
컨텍스트 기능은 그 자체만으로는
상태 관리를 하지 않기 때문입니다
이 작업은 값을 바꾸거나
컴포넌트 업데이트를 실행하지는 않습니다
대신 데이터를 컴포넌트에 펼치는 작업을 합니다
물론 이렇게 펼친 데이터는 상태 유지 중이므로
이 CartContext 컴포넌트를 통해서
이 데이터 변화를 제어합니다
그러므로 여기
이 CartContextProvider 컴포넌트에서
상태 관리가 이루어지고 나서
CartContext.Provider 컴포넌트를 반환하겠습니다
createContext로 생성된 이 컨텍스트는
Provider 속성을 가진 객체가 됩니다
이 provider 속성은 컴포넌트로서
반환값이 되어 이 컨텍스트가 필요한 컴포넌트를
감싸는 형태로 존재하게 됩니다
저는 여기에서 이 커스텀 컴포넌트로
이 컴포넌트들을 감싸서
이후에 자식 속성을 받아서
CartContext.Provider 사이의 값으로 전달하겠습니다
이제 이 함수를 내보낼 때
기본으로 내보내고(export default) 이 컨텍스트도 내보내겠습니다
다시 생각해 보니 이 함수는 기본으로
내보내지 않는 것이 좋겠습니다
대신 CartContext 객체 그 자체를 기본으로
export 하겠습니다
그리고 둘 다 export 함으로써
이 컴포넌트를 비롯해
이 컨텍스트 객체를 파일 밖에서 사용할 수 있습니다
이제 로직을 좀 더해 보겠습니다
이 컴포넌트 함수와 상태 유지 중인 로직에
항목 관리 및 추가와 삭제를 가능하게 하겠습니다
이 기능을 위해 useState를 사용할 수 있습니다
이 방식으로도 작동합니다
상태값을 전달하되
컨텍스트에서 컴포넌트로 보내는 것입니다
하지만 여기에서 다루는 상태가
이전보다 더 복잡한 종류이기 때문에
저는 여기에서 useReducer 훅(hook)을 사용하겠습니다
이 기능도 리액트가 제공하는 것으로, 상태 관리를 도와줍니다
이 기능을 사용하면 더 복잡한 상태를
더 간단하게 다룰 수 있게 되어서
상태 관리 로직을 이 컴포넌트 함수 밖으로
내보내는 것이 더 쉬워집니다
그 이유는 이 useReducer라는
리액트에서 가져오는 훅이
reducer 함수를 입력값으로 받기 때문입니다
이것을 위해 새 함수를 더하겠습니다
이름은 cartReducer로
상태와 액션을 매개변수로 받습니다
이 매개변수 값은 리액트에 의해
자동으로 이 함수로 전달됩니다
그 이유는 리액트가 후에
이 함수를 실행하기 때문입니다
cartReducer의 의의는
업데이트된 상태를 반환하는 것으로
이 액션 객체가 함수에게 상태 업데이트 방식을 알려줍니다
이 액션에서 객체는 주로 값으로 받는데
특히 이 객체는
타입(type) 속성이나 다른 식별자(identifier) 속성을 갖습니다
주로 타입이 속성명이 됩니다
이것으로 인해 이제 우리가 이 타입이라는
실행되는 액션의 일부를 확인할 수 있고
또 다양한 액션 타입에 따라 다른 코드를 실행할 수 있게 됩니다
예를 들어 이 타입은 ADD_ITEM입니다
이 고유한 식별자는 제가 만들어낸 것입니다
여러분이 임의로 정하시면 됩니다
그 이유는 여러분이 이 코드의 액션을
실행할 것이기 때문입니다
저는 여기에서 타입이 ADD_ITEM인 액션을 받겠습니다
그리고 여기에서는 상태를 업데이트해서
음식 메뉴 항목을 더하겠습니다
이에 대한 로직도 곧 작업하겠습니다
하지만 그 이전에 먼저
다른 액션 타입을 다뤄보겠습니다
타입 이름은 REMOVE_ITEM으로
방금 만든 것의 반대 역할을 합니다
즉 이 상태에서 항목을 지우는 것입니다
그리고 이 업데이트된 상태를 반환하겠습니다
이 리듀서의 끝에 도달하는 시점에
if 블록에 도달하지 않았다면
변경사항이 없는 상태를 반환하게 됩니다
이제는 여기에 항목을 더할 수 있도록 작업하겠습니다
먼저는 이 첫 if 블록에서
항목 배열에 항목을 더하겠습니다
즉 여기에서 가져오는 상태가
이후에는 이것과 같은 항목 배열을 가지게 되고
그 이유는 단순히
제가 이 프로젝트의 개발자이기 때문입니다
즉 이 상태 객체의 모양을
정의하는 작업은 모두 제게 달린 것입니다
그 이유는 이 컴포넌트 함수에서
useReducer를 호출할 때
상태 모양을 정의하는 것은 개발자이기 때문입니다
useReducer 사용을 위해서는 reducer 함수를
첫 매개변수로서 전달하므로 호출하지는 않습니다
대신 포인터를 이 함수에서
useReducer로 전달합니다
그리고 초기 상태값을 전달합니다
이 상태는
이 컴포넌트가 처음으로 렌더링될 때
useReducer에 대한 두 번째 인자로서 받아들여집니다
그래서 이 모양을 정의하는 것이
개발자에 달린 일이라고 말씀드렸습니다
이제 여기에 items 배열을 더하겠습니다
이 상태에 더할 수 있습니다
그리고 이제 이 reducer의
items 배열에 작업이 가능합니다
그 이유는 가져오는 상태 항목이
이 모양을 가지게 되기 때문입니다
그래서 이 ADD_ITEM에는
state.items으로 가서
새 항목을 배열에 추가할 수 있습니다
이 항목을 이 액션에 가지고 올 수 있습니다
예를 들면 항목 속성에 가져오는 것입니다
이 action은 다시 배정해서
배정하는 시점에 items 속성을
action에 추가할 수 있기 때문입니다
그러나 이렇게 상태를 업데이트 하는 것은 좋지 못합니다
이 방식이 부적절한 이유는 두 가지입니다
첫 번째 이유이자 둘 중 더 중요한 이유는
기존의 상태를 이렇게 변경해서는 안 되기 때문입니다
이 작업은 이미 메모리에 저장된 항목 배열을
밀어내면서 수정해 버립니다
즉 이 상태값을 변경하는 시점이
cartReducer 실행이 완료되는 시점 이전입니다
만약 여기에 코드가 추가되어 있는데
그 코드가 다른 상태를
조건부로 반환한다면
여전히 항목에는 변화가 있을 것입니다
즉 코드 실행이 끝나기 전에 변화가 생기는 것입니다
이것은 좋은 방식이 아닙니다
이 방식이 좋지 않은 두 번째 이유는
상태를 이렇게 업데이트하면
항목 배열에 새로운 항목을 더하는 것은
지양하는 것이 좋기 때문입니다
만약 한 메뉴의 '장바구니에 담기' 버튼을
여러 번 클릭했다고 해서
이 배열에 해당 메뉴를 그만큼 더하지 않는 것이 좋습니다
대신 더하는 것은 한 번만 하고
그 메뉴에 대한 양(quantity) 속성을 늘리는 것입니다
이렇게 하지 않으면
나중에 장바구니를 확인해 봤을 때
목록에 피자만 열 줄이 있을 것입니다
그 대신 피자 항목은 하나만 만들되
양, 즉 숫자만 10으로 설정하는 것입니다
그래서 상태 업데이트는 이렇게 하지 않고
일단은 items의 항목이 이미 items 배열에 있는지 확인하겠습니다
이 작업을 위해서는
existingCartItemIndex를 만들어서
state.items로 보내겠습니다
즉 이미 존재하는 배열 내 항목들입니다
그리고 여기에서 findIndex라는 자바스크립트에 내장된
메소드를 쓰겠습니다
이 메소드는 함수를 입력값으로 받습니다
이 함수는 이 배열의 각 항목에 실행되고
이 함수가 true를 반환하는 경우는
필요한 항목을 찾았을 때이고, 다른 경우는 false입니다
항목을 찾았을 경우에는
자동으로 그 항목의 인덱스를 받습니다
findIndex가 이것을 반환하게 됩니다
그리고 그 인덱스는 나중에 항목 업데이트에 사용합니다
여기서 반환하는 내용은
현재 찾고 있는 항목이 action에서 받는 항목의
ID와 일치하는지에 대한 정보입니다
이 정보를 다음 단계로 가져와서
existingCartItemIndex가 -1보다 큰지 확인하겠습니다
항목을 찾지 못했을 경우
findIndex의 반환값이 -1이기 때문입니다
만약 인덱스가 -1보다 크다면
해당 항목이 이미 배열에 있다는 것을 알 수 있습니다
다른 경우라면 그 배열에 없습니다
그러므로 이 다른 경우에서는
들어오는 action 항목을
배열에 추가합니다
하지만 state.items에 밀어넣지는 않겠습니다
이유는 이미 이전에 설명드렸습니다
대신 여기에 상수를 하나 더하겠습니다
updatedItems 상수에 배열을 추가해서
메모리에 새 배열 객체를 추가해서
기존의 items을 펼치는 것입니다
이제 이것으로 이전 배열의 복사본을 생성합니다
여기서 주목하실 부분은
items 객체는 아직 이전의 items 객체와 같지만
여기서는 문제가 되지 않습니다
이 else의 경우에는
이 새로운 복사본을 사용하되
즉 이 새로운 배열 객체를 사용해서
새 aciton 항목을 이 배열에 밀어넣을 수 있습니다
이 경우는 아직 배열에 해당 항목이
없을 때입니다
만약 항목이 배열에 있다면
양에 대한 숫자 속성을 업데이트하겠습니다
이 속성은 장바구니 항목 모두가 가지고 있습니다
이제 새로 업데이트된 항목을 추가하겠습니다
즉 새 자바스크립트 객체에
이전 항목의 속성을 펼치겠습니다
이것을 위해서는 spread라는 연산자를 사용해서
state.items를 existingCartItemIndex에 사용합니다
이것을 통해 state.items 배열에 있던
이전의 내용을 가져옵니다
그리고 이 existingCartItemIndex의 모든 속성을 가져와서
즉, 결국엔 existingCartItem에서 가져와서
이 새 객체에 펼치겠습니다
추가로 여기에 양(quantity)에 대한 속성을 추가하겠습니다
그러면 이 객체에서
기존의 항목 양을 업데이트하여 1을 추가합니다
그러므로 다시 기존 항목으로 가서
그 항목의 양 속성에 1을 더합니다
이 코드를 두 번 사용하고 있으므로
이것을 내보내서 기존 항목을 여기로 가져오겠습니다
상태 항목에 대한 existingCartItemIndex 대신
기존의 항목인 existingItem을 사용하겠습니다
또 여기에도 사용해서 코드를 더 간소화하겠습니다
물론 중요한 점은
이같은 항목의 양 속성을
그 배열의 새 항목에 더하고 있다는 점입니다
만약 그러지 않으면 이 양 속성이
미래에는 작동하지 않게 됩니다
그러므로 이 24줄의 코드를 수정하겠습니다
새 항목이 더해지면
여기에 새 객체를 추가해서
모든 메뉴를 이 객체에 펼치겠습니다
여기에는 양 속성을 더하되
초기에는 1로 설정해서 새 장바구니 항목이
언제나 1개로 시작하게 하겠습니다
만약 이후에 업데이트한다면 그 양에
하나씩 더하게 됩니다
물론
업데이트를 하는 경우에는 이 업데이트된 항목을
업데이트된 항목 배열에 다시 넣어야 합니다
그러므로 여기에서는
updatedItems로 가서 existingCartItemIndex에 접근하여
배열의 항목을 새 항목으로 대체해야 합니다
인덱스의 항목을 업데이트해야 하기 때문입니다
즉 새 항목으로 대체해야 합니다
이렇게 상태를 불변하는 방식으로 업데이트할 수 있습니다
즉 메모리의 기존 상태를 바꾸지 않고 하는 것입니다
이제 이 작업을 모두 마쳤습니다
주요한 if 블록에서 add item을 확인하는 작업이었습니다
이 안쪽 if 블록의 시작점에는
reducer의 업데이트된 상태를 반환하겠습니다
그것이 reducer의 주요 내용이기 때문입니다
업데이트된 상태를 반환해야 합니다
그리고 이것을 위해 새 객체를 반환하겠습니다
기존의 상태를 수정하기보다는
기존 상태값을 새 객체에 펼치겠습니다
현재로서 이 작업은 지나치게 반복적입니다
각 상태는 하나의 항목 배열만을 포함하고
그 배열의 내용을 바꾸고 있기 때문입니다
하지만 만약 이 상태에 다른 데이터를 더한다면
이 때에는 여기에서처럼
기존의 데이터를 이 새 상태 객체에 붙여넣어서
상태의 필요한 부분만
업데이트하는 것이 좋습니다
그 작업을 위해 항목 배열을 새 항목으로
이렇게 대치하겠습니다
이렇게 장바구니 업데이트를 위해
많은 작업을 했습니다
많은 과정이 있었지만 왜 각 과정이 필요한지
제가 충분히 설명했기를 바랍니다
이제 다음 시간에는 다음 기능인
장바구니 항목 지우기를 작업하겠습니다