FrontEnd Develop/Project : Fashion Archive

React로 만든 Fashion Archive 심층분석 #1. 이벤트핸들러 속성과 Redux 상태관리

Frisbeen 2025. 1. 1. 20:56

내가 개인적으로 혼자 하고 있는 자기의 신발 컬렉션들을 전시 및 판매하는 패션 아카이브 프로젝트 코드를 짜면서 공부한 내용이다.

 

 

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;

 

 

#결론

사실 이건 자바스크립트 문법에 가깝지만, 리액트의 이벤트 핸들링 방식과 자바스크립트의 방식은 사뭇 다르기에 헷갈릴만한 여지가 있고, 딥다이브를 하지 않는 이상 이런 중요하면서도 사소한 문제점 파악하기에는 쉽지 않을 수 있을 것 같아 나의 경험담과 도대체 왜 무한루프가 뜨지? 싶은 상황이 생긴 분들을 위해 글을 남긴다.