Github 링크
https://github.com/novice-hero/fe-sprint-coz-shopping
GitHub - novice-hero/fe-sprint-coz-shopping
Contribute to novice-hero/fe-sprint-coz-shopping development by creating an account on GitHub.
github.com
🤯 마주한 문제들
📌 북마크 기능에서의 문제
처음에 구현한 로직은 북마크 버튼을 누르면 로컬 스토리지에는 정상적으로 상품 정보가 담겼습니다.
하지만 바로 렌더링이 되지 않고 다른 상품을 북마크해야 렌더링이 되는 형식으로 구현이 되었습니다.
즉, 하나씩 밀려서 렌더링이 되었습니다.
✅ 해결
const initialState = localStorage.length
? JSON.parse(localStorage.getItem("bookmark"))
: [];
const bookmarkSlice = createSlice({
name: "bookmarkSlice",
initialState,
reducers: {
add: (state, action) => {
state.push(action.payload);
},
remove: (state, action) => {
return state.filter((v) => v.id !== action.payload);
},
},
});
북마크 된 상품들을 전역 상태로 관리하여 로컬 스토리지의 북마크 정보들에게 변경 사항이 생길 때마다 state를 즉각 업데이트를 시켜서 렌더링도 바로 업데이트 되게 해결하였습니다.
📌 북마크 페이지에서의 문제
- 메인 페이지에서 북마크 페이지로 이동했을 때, 전체 탭은 모든 상품들을 보여주는 오류가 있었습니다. 하지만 탭을 이동할 경우, 정상 작동했습니다.
- 상품 페이지에서 북마크 페이지로 이동했을 때, 상품 페이지에서 보여주던 상품들(1페이지 상품)이 북마크 페이지 1페이지에 그대로 렌더링이 되고, 2페이지에는 1페이지에 나와야 할 북마크 된 상품들이 나오는 오류가 있었습니다. 탭을 이동할 경우에도 렌더링이 된 상품 페이지의 상품들은 고정이 되고, 그 뒤는 변경이 되었습니다.
문제가 발생하던 코드
export default function BookmarkListPage() {
const filteredItem = useSelector((state) => state.productList.items);
const filteredType = useSelector((state) => state.tab.currentType);
const viewLimit = useSelector((state) => state.productList.limit);
const page = useSelector((state) => state.productList.page);
const endPage = useSelector((state) => state.productList.endPage);
const inview = useSelector((state) => state.productList.inview);
const bookmarkItem = useSelector((state) => state.bookmark);
const dispatch = useDispatch();
const [item, setItem] = useState([]);
useEffect(() => {
dispatch(tabActions.reset());
}, []);
useEffect(() => {
dispatch(productListActions.clearItems());
if (filteredType === "All") {
dispatch(productListActions.addItems(bookmarkItem));
dispatch(
productListActions.setEndPage(
Math.ceil(bookmarkItem.length / viewLimit)
)
);
} else {
dispatch(
productListActions.addItems(
bookmarkItem.filter((v) => v.type === filteredType)
)
);
dispatch(
productListActions.setEndPage(
Math.ceil(filteredItem.length / viewLimit)
)
);
}
setItem([]);
dispatch(productListActions.resetPage());
}, [filteredType, bookmarkItem]);
useEffect(() => {
if (inview) {
dispatch(productListActions.increasePage());
}
}, [inview]);
const getItems = useCallback(() => {
page < endPage &&
setItem((prev) =>
prev.concat(
filteredItem.slice(page * viewLimit, page * viewLimit + viewLimit)
)
);
}, [page, endPage, filteredItem, viewLimit]);
useEffect(() => {
getItems();
}, [getItems]);
return (
<Wrapper>
<MainContainer>
<TabList />
<ItemList data={item} />
</MainContainer>
</Wrapper>
);
}
일단 devtools로 확인했을 때, 리덕스 쪽에는 문제가 없었습니다. 타입별로 상품 정보들을 잘 걸러서 저장되고, 페이지도 잘 초기화되었습니다. 그렇다면 이 코드에 문제가 있을 것 같다는 합리적 의심이 들었습니다.
그래서 하루 내내 여러 가지를 시도해 보며 이유를 생각해 본 결과, filteredItem이 전역적으로 관리되는데 이 상태는 상품 페이지와 공유하면서 사용하기 때문에 상품 페이지에서 북마크 페이지로 넘어올 때 초기화가 안 돼서 넘어오는 건가?라는 의문이 들었습니다.
그래서 clearItem이란 리듀서를 이용해서 타입이 변경될 때마다 state를 초기화해준건데...결과는 같았습니다.
회고를 쓰면서 번뜩 생각이 들었는데, 상품 페이지 전체 탭에서 북마크 페이지 전체 탭으로 넘어가면 타입 변경이 되지 않으므로 state가 초기화가 되지 않았던 것 같습니다...😇
전역으로 관리한 상태를 두 페이지에서 공유하며 사용한 것이 결국 독이었던 것 같습니다...
이 문제에 대해 너무 시간을 오래 썼지만 명확한 해결책이 나오지 않아 현역 개발자 분에게 코드리뷰를 요청했습니다.
코드리뷰에서 말씀하시기를, 리덕스 쪽 로직은 잘 구현해주셨는데, 북마크 페이지 쪽에서 불필요한 코드가 많다, 그로 인해 가독성도 너무 떨어진다고도 해주셨습니다. 또한 filteredItem을 굳이 리덕스로 안쓰고 useMemo를 활용해서 그냥 배열을 리턴해주면 된다고 조언해 주셨습니다.
✅ 해결
조언을 받으며 리팩터링 한 코드
export default function BookmarkListPage() {
const filteredType = useSelector((state) => state.tab.currentType);
const inview = useSelector((state) => state.productList.inview);
const viewLimit = useSelector((state) => state.productList.viewLimit);
const bookmarkItem = useSelector((state) => state.bookmark);
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const filteredItem = useMemo(() => {
if (filteredType === "All") {
return bookmarkItem;
} else return bookmarkItem.filter((item) => item.type === filteredType);
}, [filteredType, bookmarkItem]);
useEffect(() => {
dispatch(tabActions.reset());
}, []);
useEffect(() => {
if (inview) setPage((prev) => prev + 1);
}, [inview]);
return (
<Wrapper>
<MainContainer>
<TabList />
<ItemList data={filteredItem.slice(0, page * viewLimit)} />
</MainContainer>
</Wrapper>
);
}
딱 봐도 가독성이 엄청 좋아진 깔끔한 코드가 되었습니다.
굳이 필요 없는 selector들을 다 쳐냈습니다. 그리고 useMemo를 이용해서 북마크 상품 정보와 타입이 변경될 때마다 타입에 맞는 북마크 상품들을 필터 했습니다.
ItemList 컴포넌트에 상품 정보들을 넘겨줄 때도 그냥 slice를 이용해서 바로 filteredItem을 0부터 page * limit까지 잘라서 넘겨주었습니다.
이렇게 하면 불필요한 렌더링과 연산이 많아지는 게 아닌가라는 생각을 했었는데, 리액트는 key 속성을 이용해서 렌더링을 할지, 안 해도 되는지 판단하기 때문에, 스크롤을 내려서 중복된 상품들을 제공해도 이미 있던 상품들이기 때문에 리렌더링을 하지 않습니다. 또한 사용자가 스크롤을 많이 해봤자 몇천만, 몇억 개를 추가하지 않기 때문에 상관없을 것이라고 생각했습니다.
프로젝트를 진행하면서 리덕스를 연습하며 익숙해지기 위해서 최대한 리덕스를 사용하려고 했는데, 오히려 리덕스를 사용해야 한다고 강박이 생기고 생각이 굳어버렸던 것 같습니다...ㅠ
뭐든지 적당히 하는 게 좋은 것이라는 것을 다시 또 배웠습니다.
📌 무한 스크롤 기능에서의 문제
export default function ProductListPage() {
const filteredItem = useSelector((state) => state.productList.items);
const filteredType = useSelector((state) => state.tab.currentType);
const viewLimit = useSelector((state) => state.productList.limit);
const page = useSelector((state) => state.productList.page);
const endPage = useSelector((state) => state.productList.endPage);
const inview = useSelector((state) => state.productList.inview);
const dispatch = useDispatch();
const [item, setItem] = useState([]);
useEffect(() => {
dispatch(tabActions.reset());
}, []);
useEffect(() => {
const fetchAllData = async () => {
const data = await cozShoppingApi.getAllItem();
dispatch(productListActions.clearItems());
if (filteredType === "All") {
dispatch(productListActions.addItems(data));
dispatch(
productListActions.setEndPage(Math.ceil(data.length / viewLimit))
);
} else {
dispatch(
productListActions.addItems(
data.filter((v) => v.type === filteredType)
)
);
dispatch(
productListActions.setEndPage(
Math.ceil(filteredItem.length / viewLimit)
)
);
}
setItem([]);
dispatch(productListActions.resetPage());
};
fetchAllData();
}, [dispatch, filteredItem.length, filteredType, viewLimit]);
useEffect(() => {
if (inview) {
dispatch(productListActions.increasePage());
}
}, [inview]);
const getItems = useCallback(() => {
page < endPage &&
setItem((prev) =>
prev.concat(
filteredItem.slice(page * viewLimit, page * viewLimit + viewLimit)
)
);
}, [page, endPage, filteredItem, viewLimit]);
useEffect(() => {
getItems();
}, [getItems]);
return (
<Wrapper>
<MainContainer>
<TabList />
<ItemList data={item} />
</MainContainer>
</Wrapper>
);
}
북마크 페이지는 오류가 있었지만, 상품 페이지는 오류가 없었습니다.
하지만 위 코드를 보면 알 수 있듯이, 코드의 가독성이 아주 개떡 같았습니다.
그래서 북마크 페이지를 리팩터링 한 것을 기반으로 상품 페이지 또한 리팩터링 하게 됐습니다.
export default function ProductListPage() {
const filteredType = useSelector((state) => state.tab.currentType);
const inview = useSelector((state) => state.productList.inview);
const viewLimit = useSelector((state) => state.productList.viewLimit);
const items = useSelector((state) => state.productList.items);
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const filteredItem = useMemo(() => {
if (filteredType === "All") {
return items;
} else return items.filter((item) => item.type === filteredType);
}, [filteredType, items]);
useEffect(() => {
dispatch(tabActions.reset());
}, []);
useEffect(() => {
const fetchAllData = async () => {
const data = await cozShoppingApi.getAllItem();
dispatch(productListActions.updateItems(data));
};
fetchAllData();
}, [dispatch, filteredType]);
useEffect(() => {
if (inview) setPage((prev) => prev + 1);
}, [inview]);
return (
<Wrapper>
<MainContainer>
<TabList />
<ItemList data={filteredItem.slice(0, page * viewLimit)} />
</MainContainer>
</Wrapper>
);
}
아주 보기 좋습니다.
하지만 상품 페이지는 북마크 페이지와는 다르게 서버에 데이터를 요청해서 받아오는 로직이 있었기 때문에 약간 달랐습니다.
사실 데이터를 받아와서 저장할 때, 굳이 리덕스를 안 써도 되지만... 있으니까 써보았습니다...😅
하지만 여기에도 문제가 한 가지 있었습니다.
최초 로딩 후 렌더링 시에 상품 정보들을 1페이지가 아니라 2페이지까지 한 번에 불러와서 렌더링 하는 것입니다.
왜 그런가 생각을 해보니, 상품 페이지는 처음 데이터가 없고 서버에서 받아와서 보여주는 것이기 때문에, 최초 렌더링 시에 푸터에 있는 옵서버가 위로 올라오게 되어 감지되게 됩니다. 따라서 페이지가 2로 증가하게 되어 2 * limit 만큼 렌더링 되었던 것입니다.
✅ 해결
const filteredItem = useMemo(() => {
setPage(1);
if (filteredType === "All") {
return items;
} else return items.filter((item) => item.type === filteredType);
}, [filteredType, items]);
이 부분에 setPage(1)을 하여 페이지를 1로 고정시켜 주어 해결했습니다.
😋 추가해 보면 좋을 것들
- 매직 넘버, 타입 문자열들 상수화 시키기
- TOP 버튼 만들기
- 토스트 클릭해서 삭제할 수 있게 해 보기
- 다크모드
🗒️ 배운 점
- 요구 사항 명세서를 기반으로 티켓을 생성하고 플래닝 포커를 해보았습니다.
- 제공받은 피그마를 기반으로 개발을 해보았습니다.
- git-flow 기반의 개발 방식에 대해서 공부하고 적용해 볼 수 있었습니다.
- 폴더 구조를 나누어보았습니다.
- 재사용이 가능한 컴포넌트를 만들어서 적용해 보았고, 최대한 컴포넌트를 쪼개어 구현했습니다.
- Redux Toolkit에 익숙해졌습니다.
- 코드리뷰를 직접 받아보고, 리뷰 기반으로 수정도 해보았습니다.
'Project' 카테고리의 다른 글
[하루메이트] 오늘의 회고 - 22.07.12 (0) | 2023.07.12 |
---|---|
[하루메이트] react-beautiful-dnd로 드래그 앤 드롭 적용하기 (4) | 2023.07.12 |
[하루메이트] 오늘의 회고 - 22.07.10 (0) | 2023.07.10 |
[하루메이트] 오늘의 회고 - 22.07.05 (0) | 2023.07.05 |
[영화 검색 & 추천 사이트] 회고 (3) | 2023.06.05 |