내가 개인적으로 혼자 하고 있는 자기의 신발 컬렉션들을 전시 및 판매하는 패션 아카이브 프로젝트 코드를 짜면서 공부한 내용이다.
https://github.com/ChoiTheCreator/TheFashionArchive
#리액트 코드를 짜다가 무한루프에 빠질수가 있을까?
물론 알고리즘 공부하면서 코드 잘못짜서 while문에 늪에 빠진적은 있었을것이다. 그러나 내 개인프젝에서 렌더링이 아예 안되는 이런 경험은 처음이었다.
먼저 화면들을 보자면 아래와같다.
#메인화면
여기서 장바구니 (Carts)를 클릭하면, 장바구니 페이지로 넘어가는데, 여기서 무한루프가 발생해서 크롬 브라우저가 먹통이 되는 것이다.
#장바구니 페이지
#무엇이 문제였을까?
1. Redux 상태 관리 특성:
Redux는 상태(state)를 중앙에서 관리하며, 상태가 변경될 때 관련 컴포넌트를 다시 렌더링한다.
useDispatch를 통해 액션을 디스패치하면 상태가 변경되고, useSelector로 해당 상태를 구독하는 컴포넌트가 업데이트된다
/*eslint-disable */
import { configureStore, createSlice } from '@reduxjs/toolkit';
//이건 useState의 역할 (user이라는 state가 전역으로 쓸 수 있게 됨)
//1. 이게 첫번째 global state 등록본 user
const user = createSlice({
name: 'user',
initialState: 'kim',ㅋ
});
//2. second global state 등록본
const stock = createSlice({
name: 'stock',
initialState: ' 500 dollars',
reducers: {
changeName(state) {
return 'thomas i will change yours name as' + state;
},
},
});
//3. third global state (OBJ FUCK)
const userCart = createSlice({
name: 'userCart',
//initialState가 우리가 원하는 globalState임. 이때 객체형태로 하려면 [이 안에다가 박음]
initialState: [
{ id: 0, name: 'White and Black', count: 2 },
{ id: 2, name: 'Grey Yordan', count: 1 },
],
reducers: {
increaseCount(state, action) {
const findItemId = state.find((item) => item.id === action.payload);
if (findItemId) {
findItemId.count += 1;
}
},
},
});
export default configureStore({
reducer: {
user: user.reducer,
stock: stock.reducer,
userCart: userCart.reducer,
},
});
export let { changeName } = stock.actions;
export const { increaseCount } = userCart.actions;
위와 같은 코드를 활용했었다.
2. dispatch 즉시 실행으로 발생한 문제:
• 코드에서 onClick={dispatch(increaseCount(item.id))}처럼 dispatch를 직접 호출하면, 렌더링 시 dispatch가 즉시 실행된다.
• Redux 상태가 변경되고 컴포넌트가 다시 렌더링되면서, 또 dispatch가 호출되는 무한 루프가 발생한다.
##이렇게 말하면 어려울테니
React에서 onClick 속성에 즉시 호출 함수를 전달하면, 컴포넌트 렌더링 시 그 함수가 실행된다.
이로 인하여, Redux 상태 변경과 리액트 재렌더링 메커니즘이 충돌하게 되는데..
##이렇게 말해도 어려울테니 더 쉽게 말하면
우리가 onClick 함수를 만들어서 기대하는 효과는 어떤 DOM 요소를 클릭했을때, onClick ={ 내부함수} 가 작동되길 기대한다.
그러나 만약 onClick ={내부함수()} 이렇게 쓴다면 이건 즉시 실행 함수이며 내부함수() 의 실행결과를 onClick의 속성으로 전달하는 것이다.
#이러면 왜 안될까?
즉시실행함수를 onClick의 속성에다가 넣어버리면, 이는 이벤트 동작과 무관하게 리액트가 렌더링 시에 내부함수를 실행해서 반환값을 onClick에 저장한다. 즉 클릭이벤트가 아닌!! 렌더링 시 실행된다는 것이다.
즉 우리의 목적과는 다르게 실행되니 당황을 할 수밖에 없다는 것이다.
#Redux 상태관리와 함께 쓴다면? -> 최악의 상황 발생
React의 렌더링 흐름
• React는 컴포넌트를 렌더링할 때 JSX를 읽고 평가한다.
• onClick에 즉시 실행 함수가 있으면, React는 클릭 이벤트를 기다리지 않고 바로 실행한다.
Redux 상태 관리
• Redux는 dispatch를 통해 상태가 변경되면 상태를 사용하는 컴포넌트를 다시 렌더링한다
• 다시 렌더링되면 JSX가 평가되므로 dispatch가 또 호출...
결과적으로, React의 렌더링 → 상태 변경 → 다시 렌더링 과정이 계속 반복되어 무한 루프가 발생한다는 것이다!
# 어떻게 해결하나?
사실 이렇게 즉시 실행함수처럼 쓴 이유는 onClick의 콜백함수를 쓰기 위해서 그런것인데, 해당 콜백함수에 매개변수가 필요했던 것.
즉 콜백함수를 매개변수가 필요하니 () 를 써서 즉시 실행함수 처럼 보이게 리액트를 헷갈리게 한 것이다.
따라서 리액트가 헷갈리지 않게 즉시 실행 함수처럼 보이는 우리의 함수를 화살표로 함수로 줘버리면
리액트가 아 이건 함수구나! 할 수 있는 것.
-> 이러면 무한루프에 빠질 일도 없고, 쉽게 해결할 수 있겠다.
#원래 썼던 코드
import React from 'react';
import '../Style/CartPage.css'; // CSS 파일 임포트
import { Table } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { increaseCount } from '../Store/store';
const CartPage = () => {
const userData = useSelector((state) => state.stock);
console.log(userData);
let dispatch = useDispatch();
const userCart = useSelector((globalState) => globalState.userCart);
console.log(userCart);
return (
<div className="cart-container ">
<Table striped bordered hover className="cart-table">
<thead>
<tr>
<th>#</th>
<th>Product ID</th>
<th>Quantity</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{userCart.map((item) => (
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.name}</td>
<td>{item.count}</td>
<td>
<button
className="edit-btn"
onClick={dispatch(increaseCount(item.id))} // 잘못된 부분
>
수량 추가
</button>
</td>
</tr>
))}
</tbody>
</Table>
</div>
);
};
export default CartPage;
#올바른 코드
import React from 'react';
import '../Style/CartPage.css'; // CSS 파일 임포트
import { Table } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { increaseCount } from '../Store/store';
const CartPage = () => {
const userData = useSelector((state) => state.stock);
console.log(userData);
let dispatch = useDispatch();
const userCart = useSelector((globalState) => globalState.userCart);
console.log(userCart);
return (
<div className="cart-container ">
<Table striped bordered hover className="cart-table">
<thead>
<tr>
<th>#</th>
<th>Product ID</th>
<th>Quantity</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{userCart.map((item) => (
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.name}</td>
<td>{item.count}</td>
<td>
<button
className="edit-btn"
onClick={() => dispatch(increaseCount(item.id))} // 함수로 감싸기
>
수량 추가
</button>
</td>
</tr>
))}
</tbody>
</Table>
</div>
);
};
export default CartPage;
#결론
사실 이건 자바스크립트 문법에 가깝지만, 리액트의 이벤트 핸들링 방식과 자바스크립트의 방식은 사뭇 다르기에 헷갈릴만한 여지가 있고, 딥다이브를 하지 않는 이상 이런 중요하면서도 사소한 문제점 파악하기에는 쉽지 않을 수 있을 것 같아 나의 경험담과 도대체 왜 무한루프가 뜨지? 싶은 상황이 생긴 분들을 위해 글을 남긴다.
'FrontEnd Develop > Project : Fashion Archive' 카테고리의 다른 글
📚 React Query, 과연 써야 하는가? 그리고 구조분해할당 친절한 분석 (2) | 2025.01.15 |
---|---|
Local Storage 사용법과 이를 활용한 최상위 컴포넌트(App.jsx)에서의 책임 분리 (1) | 2025.01.14 |