근래 리액트 강의를 통해 개념적인 토대를 어느정도 마련했습니다.
저는 이러한 개념들을 실제로 적용해보는 것이 더 깊이 공부하는 것의 시작이라고 생각합니다.
그래서 이전 Week Review-3에서도 언급했다시피 무언가를 만들어보려고 결심했고,
CRUD를 녹여낼 수 있는 프로젝트가 ‘재고 관리 시스템’이라고 생각했습니다.
(게시판, todo리스트는 너무 흔해서 손이 안갔습니다..)
그래서 지난 1주일간 이를 만들어보려고 노력했으나, 마음에 들지 않는 부분이 많았습니다.
이번 회고록을 통해 아쉬운 지점들을 살펴보고 이를 개선하여 ‘새로운 재고 관리 시스템’을 만들어볼까 합니다.
1. 프로젝트 구성
우선 해당 프로젝트(?)는 이전에 배운 리액트 개념을 실제로 적용해보는 것이 목표였습니다.
따라서, ‘재고 관리 시스템’이지만 실제로는 데이터베이스와 통신하는 백엔드는 별도로 존재하지 않습니다.
해당 프로젝트에 등록되는 아이템들은 변수를 통해 관리되고 오직 React를 이용해서 개발을 진행했음을 말씀드립니다.
2. 재고 관리 시스템 기능
1. 네비게이션 바
2. 검색
3. 재고 리스트 출력
4. 재고 등록
5. 재고 이동
6. 재고 삭제
위 6가지 기능을 구현하여 React 개념을 적용해보고자 하였습니다.
2-1. 상세 동작
1) 검색
검색은 사용자가 입력한 값을 실시간으로 받아서 리스트에 출력하는 것이 아니라, 사용자의 인풋을 모두 받고 ‘검색 버튼’을 눌렀을 때 검색이 진행되도록 기능을 구현하고자 하였습니다.
2) 재고 등록
재고는 id, name, barcode, location, quantity, isChecked라는 프로퍼티를 가진 객체로 관리되고 있습니다.
사용자로부터 재고명 (name), 바코드 (barcode), 재고 위치 (location), 재고 수량 (quantity)를 입력받아서 재고를 새롭게 등록합니다.
검색과 마찬가지로 사용자로부터 인풋을 모두 입력받은 후, 재고 등록 버튼을 눌러야 비로소 재고가 추가되도록 구현하고 싶었습니다.
여기서 검색과는 다르게, 주어진 인풋이 모두 입력되지 않으면 재고 추가가 되지 않고, focus가 되도록 구현하고자 했습니다.
3) 재고 이동
재고 이동의 경우에는 재고를 하나씩 이동하는 것이 아니라, 사용자에게 재고 리스트를 전체적으로 보여주고 Update하고자하는 데이터를 모두 입력 받습니다.
이후, 재고 이동 버튼을 눌렀을 때 입력받은 데이터에 맞춰서 재고 이동이 진행되도록 구현하고 싶었습니다.
4) 재고 삭제
재고 삭제의 경우에도 단순히 체크를 했을 때 아이템을 삭제하는 것이 아니라, 주어진 재고 리스트에서 삭제를 원하는 재고들을 모두 체크한 이후 삭제 버튼을 눌렀을 때 재고에서 삭제되도록 구현하고 싶었습니다.
3. 동작 예시
3-1. 검색
3-2. 재고 등록
3-3. 재고 이동
3-4. 재고 삭제
4. 회고록
제가 해당 재고 관리 시스템을 개발하면서 고민했던 부분이 여러 지점이 있는데 이에 대해서 이야기해보고자 합니다.
4-1. 검색 기능 분리
첫 번째로 시도한 부분은 검색 기능을 재고 목록을 보여주는 리스트와 분리하고자 하였습니다.
그 이유는 검색바와 리스트를 각각의 컴포넌트로 분리한다면 검색 기능을 다른 곳에서도 재사용할 수 있지 않을까하는 생각 때문이었습니다.
하지만, 컴포넌트는 트리 형태로 생성되기 때문에 List와 SearchBar를 동일한 Level에 위치시키면 두 컴포넌트는 서로 데이터를 상호작용할 수 없습니다.
즉, 두 컴포넌트 사이를 연결해줄 부모 컴포넌트가 필요하게 됩니다.
따라서, SearchBar와 List를 동시에 보여줄 Edit 페이지에 검색 데이터를 관리할 새로운 변수를 생성하고 관리하기로 결정했습니다.
Edit.jsx
import List from "../components/List";
import SearchBar from "../components/SearchBar";
import { useState, createContext, useContext } from "react";
import "./Eidt.scss";
import Button from "../components/Button";
import { ItemDispatchContext } from "../App";
export const FilteredStateList = createContext();
export const FilteredDispatchList = createContext();
export const UpdateStateList = createContext();
export const UpdateDispatchList = createContext();
const Edit = () => {
const [filteredList, setFilteredList] = useState([]);
const [updateList, setUpdateList] = useState([]);
const { onUpdate } = useContext(ItemDispatchContext);
const onClickUpdateButton = () => {
updateList.forEach((item) => {
onUpdate({ ...item });
});
};
return (
<div className="Edit">
<FilteredStateList.Provider value={filteredList}>
<FilteredDispatchList.Provider value={setFilteredList}>
<SearchBar text={"검색"} />
<Button text={"재고 이동"} onClick={onClickUpdateButton} />
<UpdateStateList.Provider value={updateList}>
<UpdateDispatchList.Provider value={setUpdateList}>
<List filteredList={filteredList} />
</UpdateDispatchList.Provider>
</UpdateStateList.Provider>
</FilteredDispatchList.Provider>
</FilteredStateList.Provider>
</div>
);
};
export default Edit;
위 코드에서 filteredList라는 변수에 SearchBar에서 사용자로부터 받은 내용으로 필터링하여 저장하도록 코드를 작성했습니다.
따라서, List 컴포넌트는 filteredList를 props로 받아서 출력하도록 코드를 작성했습니다.
여기서 한 가지 아쉬운 지점은 굳이 createContext를 사용할 필요가 없었다는 것입니다.
SearchBar에는 하위 컴포넌트가 존재하지 않기 때문에 props drilling이 발생하지 않습니다.
props로 전달받는 것이 오히려 재사용성 측면에서 조금 더 낫지 않았을까 생각합니다.
4-2. 재고 추가 기능 id 설정
이전에 소개해드린 내용에 따르면 재고를 등록할 때 사용자로부터 재고의 id는 받지 않습니다.
그래서 재고 목록을 관리하고 있는 최상위 컴포넌트인 App.jsx에서 onCreate 함수를 생성하여 id값을 숫자 하나씩 증가시키는 방식으로 사용하고 있었습니다.
하지만, 추후 기능을 구현하다보니 발생하는 문제가 있었습니다.
만약 ‘감자탕’이라는 상품이 A 로케이션에 이미 존재하고 있는 상황에서 동일하게 ‘감자탕’이라는 상품을 A 로케이션에 추가하면 다른 상품으로 분류가 됩니다.
마찬가지로 재고 이동에서도 동일한 상품을 다르게 분류하는 문제가 발생할 수 있습니다.
이는 동일한 상품임에도 id가 다르기 때문에 발생하는 문제였습니다.
따라서, id를 상품의 고유한 값인 바코드와 로케이션을 합친 값을 사용하기로 결정했습니다.
재고 추가 페이지인 Add 컴포넌트에서는 추가된 상품들을 배열에 저장하고 있습니다.
Add 컴포넌트가 언마운트되는 시점에 해당 배열의 요소를 순회하며 기존 재고 리스트에 추가합니다.
만약 기능을 추가한다면, Add 컴포넌트에서 언마운트 이전까지 가지고 있는 배열 값들을 활용하여 현재 사용자가 추가하고 있는 재고들을 보여주는 기능까지 구현할 수 있을 것 같습니다.
아래는 제가 작성한 Add 컴포넌트 코드입니다.
이전에 말씀드린 id 생성 과정은 onChange에서 진행됩니다.
([‘id’]를 통해 인풋값들을 조합하여 새로운 id를 생성합니다.)
Add.jsx
import "./Add.scss";
import { useState, useContext, useRef, useEffect } from "react";
import { ItemDispatchContext, ItemStateContext } from "../App";
import Button from "../components/Button";
const Add = () => {
const [addItem, setAddItem] = useState({
name: "",
barcode: "",
location: "",
quantity: "",
isChecked: false,
});
const { onCreate } = useContext(ItemDispatchContext);
const { idRef, items } = useContext(ItemStateContext);
const [newItemList, setNewItemList] = useState([]);
const nameRef = useRef("");
const barcodeRef = useRef("");
const locationRef = useRef("");
const quantityRef = useRef("");
const onChange = (e) => {
setAddItem((prevItem) => {
return {
...prevItem,
["id"]: `${addItem.barcode}+${addItem.location}`,
[e.target.name]: e.target.value,
};
});
};
const onClickAddItem = () => {
if (addItem.name === "") {
nameRef.current.focus();
return;
} else if (addItem.barcode === "") {
barcodeRef.current.focus();
return;
} else if (addItem.location === "") {
locationRef.current.focus();
return;
} else if (addItem.quantity === "") {
quantityRef.current.focus();
return;
}
setNewItemList(
newItemList.find(
(item) =>
item.barcode === addItem.barcode && item.location === addItem.location
)
? newItemList.map((item) => {
if (
item.barcode === addItem.barcode &&
item.location === addItem.location
) {
return {
...item,
quantity: item.quantity + addItem.quantity,
};
}
return item;
})
: [...newItemList, addItem]
);
};
useEffect(() => {
return () => {
newItemList.forEach((item) => {
onCreate({ ...item });
});
};
}, [newItemList]);
return (
<div className="Add">
<input
ref={nameRef}
name="name"
value={addItem.name}
placeholder="상품명.."
onChange={onChange}
></input>
<input
ref={barcodeRef}
name="barcode"
value={addItem.barcode}
placeholder="바코드.."
onChange={onChange}
></input>
<input
ref={locationRef}
name="location"
value={addItem.location}
placeholder="로케이션.."
onChange={onChange}
></input>
<input
ref={quantityRef}
name="quantity"
value={addItem.quantity}
placeholder="수량.."
onChange={onChange}
></input>
<Button text={"상품 추가"} onClick={onClickAddItem} />
</div>
);
};
export default Add;
4-3. 재고 이동 데이터를 어떻게 관리해야 하는가?
코드를 작성하면서 각 페이지 컴포넌트에서 변경 데이터를 관리하는 것이 적절하다고 생각했습니다.
예를들어, 추가한 재고 데이터는 Add 컴포넌트에서 관리를 하고, 이동시키고자 하는 재고 데이터는 Edit 컴포넌트에서 관리하고, 삭제한 데이터는 Delete 컴포넌트에서 관리하는 식입니다.
왜냐하면 해당 데이터들은 하위 컴포넌트에 대해서만 전체적으로 영향을 미치기 때문에 영향을 미치는 컴포넌트들에서 가장 가까운 최상위 노드에 위치해야 데이터 관리가 편하기 때문입니다.
마찬가지로 재고 이동의 경우에도 Edit 컴포넌트에서 변경하고자하는 데이터들을 관리하고자 했습니다.
하지만 이러한 관점을 유지하면서 코드를 작성하는 것이 생각보다 까다로웠습니다.
4-1에 첨부한 Edit.jsx 코드를 살펴보면 재고 이동 데이터를 관리하기 위해 변수 updateList를 선언해놓은 상태입니다.
Edit 컴포넌트 트리는 아래 사진과 같이 구성되어 있습니다.
이전에 말씀드린 바와 같이 updateList에 사용자가 입력한 변경 데이터를 저장하고 한번에 update를 요청하려고 합니다.
하지만, 사용자로부터 데이터를 입력받는 input 태그는 Item 컴포넌트에 존재합니다.
현재 Item 컴포넌트는 Edit 페이지 뿐만 아니라 Delete 페이지에서도 재사용하고 있는 컴포넌트이므로 Item 컴포넌트안에 로직을 넣고싶지는 않습니다.
( 2-1. 3)에서 설명드린 순서와 같이 동작하도록 구현하고 싶습니다. )
이러한 지점에서 어떻게 해결해야할지 고민하다보니 아래와 같은 아이디어를 생각해봤습니다.
1. onChange 함수를 List 컴포넌트에서 정의한다.
- Item 컴포넌트에 로직을 넣지 않기 위함이다.
- onChange 함수는 Item 컴포넌트에서 사용자로부터 데이터를 입력받는 input 태그의 name과 value를 활용한다.
- 주요 동작은 아래와 같다.
1) onChange 함수는 e와 id를 매개변수로 전달받는다.
2) updateItems 배열에서 id와 매칭되는 요소의 index를 반환한다.
3) 해당 index에 접근해서 객체의 프로퍼티를 아래와 같이 수정한다.
4) 사용자가 입력한 input 태그의 name이 존재하지 않으면 프로퍼티를 생성하고 value를 저장한다.
5) 사용자가 입력한 input 태그의 name이 존재한다면 해당 프로퍼티에 value를 저장한다.
2. updateItems는 Edit 컴포넌트에서 가지고 있는 변수이므로 특정 버튼을 누르면 배열을 순회하며 update 함수를 실행한다.
이렇게 코드를 작성하면서 name과 value라는 속성을 조금 더 활용할 수 있게 된 것 같습니다.
다만, 결국 List 컴포넌트 안에 update와 관련된 로직이 추가되었습니다.
List 컴포넌트 또한 다른 컴포넌트에서 재사용할 수 있는 컴포넌트이지만, 과연 List 컴포넌트를 무조건 재사용하는게 맞을까라는 의문도 들게 되었습니다.
모든 List가 동일한 포맷을 사용하는 것이 아니기 때문에 각 페이지별로 List 컴포넌트를 만드는게 더 나은 방법인가 고민을 해봐야할 것 같습니다.
4-4. 삭제할 재고를 어떻게 판별하는가?
다음으로는 재고 삭제입니다. 등록된 재고의 리스트에서 checkbox를 통해 삭제하고자 하는 아이템들을 체크하고, 재고 삭제 버튼을 누르면 체크된 재고들이 모두 삭제되도록 구현하고자 하였습니다.
여기서 고민했던 부분은 ‘checkBox의 선택 여부를 어떻게 판별할 것인가?’ 였습니다.
제가 생각한 아이디어는 결국 item 객체에 ‘isChecked’라는 프로퍼티를 추가하고 체크박스의 체크 여부에 따라 True/False로 변경되도록 하는 것이었습니다.
하지만, 저는 여기서 TOGGLE_ITEM이라는 함수를 App.jsx에서 선언하여 checkBox의 onClick 이벤트에 넘겨주었습니다.
해당 TOGGLE_ITEM은 재고 목록에 map 메서드로 직접 접근하여 isChecked 프로퍼티를 변경하고 있습니다.
등록된 재고가 많다면 당연히 속도적인 측면에서 문제가 발생할 수 있습니다.
따라서, Delete 컴포넌트에서는 checkBox에서 체크된 아이템들을 배열에 저장하여 한번에 Delete하도록 만드는 방식으로 업데이트를 할 수 있을 것 같습니다.
4-5. 아키텍쳐에 대한 고민
이렇게 직접 재고 관리 시스템을 작성하면서 가장 신경쓰고자 했던 부분은 props drilling을 최소화하고자 하는 것이었습니다.
이 부분을 생각하다보니 아키텍쳐에 대한 고민까지 이어졌습니다.
이번 재고 관리 시스템을 만들면서 아쉬운 점이 있었다면 막무가내 코딩으로 코드를 작성하는 거셍 맞춰서 수정하는 부분이 너무 많았다는 것입니다.
다음에는 전체적인 다이어그램을 그리고나서 새로운 재고 관리 시스템을 만들어볼까 합니다.
Github : InventoryManagementSystem