본문 바로가기

Redux

[Redux] redux state undefined 에러 해결하기(feat. hoisting)

이번에 redux와 vanilla-Js 만을 사용해 counter 앱을 만들어보는 과정에서 발생한 에러를 포스트로 남겨두려고 한다.

에러 내용


Uncaught TypeError: Cannot read properties of undefined (reading 'toggle')

다음 에러는 "toggle을 읽으려는데 undefined의 프로퍼티는 읽을 수 없어요!"라고 말하고 있다.

밑에 코드를 기술할 테지만 먼저 index.js의 55줄을 보면 다음과 같은 코드에서 에러가 난 것이다.

    if (state.toggle) { //state.toggle을 읽어오는 과정에서 에러 발생
        divToggle.classList.add("active");
    } else {
        divToggle.classList.remove("active");
    }

그러면 이렇게 이해할 수 있다.

"state의 프로퍼티인 toggle을 읽어오려는데 아직 state가 undefined라서 toggle을 읽어올 수가 없구나!"

코드 분석


실제로 console.log(state)를 찍어보면 state의 초깃값은 undefined가 나온다. 그래서 state 초깃값을 잘 설정해 주었는지 확인해 보았다.

import { createStore } from "redux";
const store = createStore(reducer);

... // 리덕스 액션 함수 등등

const initialState = { //초깃값 설정해줄 변수
    toggle: false,
    counter: 0,
};
function reducer(state = initialState, action) { //state의 초깃값을 initalState로 초기화
    console.log(state); //undefined
    ...
}

... // 이벤트 설정 함수들

const render = () => {...};
render();
store.subscribe(render);

확실히 const initialState = {toggle: false, counter: 0, }; 로 초깃값을 설정하고 reducer 함수에서 state=initialState로 초기화해주었다. 그런데 왜 안되는 걸까? 위 코드에서 문제점을 알았다면 hoisting의 개념을 잘 알고 있는 것이다. 문제부터 말해보자면 initialState 변수가 store 변수 이후에 선언된 것이 잘못되었다. 위 코드의 흐름을 보면 다음과 같다.

  1. 모든 변수는호이스팅에 의해 코드 위로 올라가서 "선언"만 되어있는 상태이다. 마찬가지로 initalState도 선언만 되고 undefined인 상태다.
  2. 모든 함수들도 호이스팅에 의해 위로 올라간다. 마찬가지로 reducer 함수도 위로 올라간다.
  3. const store = createStore(reducer); 에서 reducer 함수를 불러온다.
  4. reducer 함수의 state를 initialState로 초기화해주는데 이 initialState도 아직 undefined다.
  5. 뒤늦게 const initialState = {toggle: true, counter: 0};을 만나서 initialState가 초기화가 된다.
  6. reducer안의 state는 undefined인 상태로 여러 함수들에 사용되면서 state.toggle, state.counter 등의 코드에 에러를 띄운다.

여기서 문제는 뒤늦게 initialState가 초기화되었다는 점이다. 그렇다면 문제 해결을 위해 initialState를 reduce 함수가 실행되기 전에 값을 제대로 초기화해주면 된다.

//앞뒤코드 생략
const initialState = {
    toggle: false,
    counter: 0,
};
const store = createStore(reducer);

이렇게 되면 initialState가 초기화된 후 reducer가 실행되므로 reducer에서 state = initialState가 정상적으로 원하는 변수가 들어간다.

문제 해결~!

참고용 코드


<index.js>

import { createStore } from "redux";
const store = createStore(reducer);
const initialState = {
    toggle: false,
    counter: 0,
};
const divToggle = document.querySelector(".toggle");
const counteras = document.querySelector("h1");
const btnIncrease = document.querySelector("#increase");
const btnDecrease = document.querySelector("#decrease");

const TOGGLE_SWITCH = "TOGGLE_SWITCH";
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";

const toggleSwitch = () => ({ type: TOGGLE_SWITCH });
const increase = (difference) => ({ type: INCREASE, difference });
const decrease = () => ({ type: DECREASE });

function reducer(state = initialState, action) {
    console.log(state);
    switch (action.type) {
        case TOGGLE_SWITCH:
            return {
                ...state,
                toggle: !state.toggle,
            };
        case INCREASE:
            return {
                ...state,
                counter: state.counter + action.difference,
            };
        case DECREASE:
            return {
                ...state,
                counter: state.counter - 1,
            };
        default:
            return state;
    }
}

divToggle.onclick = () => {
    store.dispatch(toggleSwitch());
};
btnIncrease.onclick = () => {
    store.dispatch(increase(1));
};
btnDecrease.onclick = () => {
    store.dispatch(decrease());
};

const render = () => {
    const state = store.getState();
    if (state.toggle) {
        divToggle.classList.add("active");
    } else {
        divToggle.classList.remove("active");
    }
    console.log(state.counter);
    counteras.innerText = state.counter;
};

render();
store.subscribe(render);

<index.html>

<html>
    <head>
        <link rel="stylesheet" type="text/css" href="index.css" />
    </head>
    <body>
        <div class="toggle"></div>
        <hr />
        <h1>0</h1>
        <button id="increase">+1</button>
        <button id="decrease">-1</button>
        <script src="./index.js"></script>
    </body>
</html>