2차 공부/TIL

24.07.03 TodoList페이지에 redux 사용하기

공대탈출 2024. 7. 3. 17:51

기존 TodoList는 props를 자식 컴포넌트에 넘겨주어 상태를 관리했다.

이번엔 redux 라이브러리를 사용하여 전역 store에 state를 저장하고, 그것을 가져와 사용하는 것을 적용해보고자 한다.

 

먼저, redux 폴더를 만들고 config, modules폴더를 만들어 configStore, todo파일을 생성했다.

 

그리고 todo.jsx파일에 리듀서코드를 작성했다.

Todo는 추가, 제거, 완료/취소 기능만 구현하면 되므로, 세가지 ActionValue를 만들어 기능을 구현하고자 한다.

// src/modules/todo.js

//Action Value
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
//Action Creator
export const addTodo = (payload) => {
    return {
        type: ADD_TODO,
        payload,
    };
};
export const toggleTodo = (payload) => {
    return {
        type: TOGGLE_TODO,
        payload,
    };
};
export const deleteTodo = (payload) => {
    return {
        type: DELETE_TODO,
        payload,
    };
};
//Initial State
const initialState = {
    todo: [
        {
            title: '스타일드 컴포넌트 공부하기!',
            desc: 'props 받아와 스타일 작성 공부하기',
            isDone: false,
            id: 'asdf-1234-asdf-1234',
        },
        {
            title: '자바스크립트 공부하기!',
            desc: '이벤트 핸들러 공부하기',
            isDone: true,
            id: 'asdf-2345-asdf-2345',
        },
    ],
};
//Reducer
const todoState = (state = initialState, action) => {
    switch (action.type) {
        case ADD_TODO: {
            return {};
        }
        case DELETE_TODO: {
            return {};
        }
        case TOGGLE_TODO: {
            return {};
        }
        default:
            return state;
    }
};
//export default reducer
export default todoState;

 

그리고 해당 리듀서를 스토어에 연결해주었다.

import { createStore } from 'redux';
import { combineReducers } from 'redux';
import todoState from '../modules/todo';

const rootReducer = combineReducers({ todoState: todoState });
const store = createStore(rootReducer);

export default store;

 

//InputBox.jsx
    const [title, setTitle] = useState('');
    const [desc, setDesc] = useState('');
    // const [id, setId] = useState(crypto.randomUUID());
    // const [isDone, setIsDone] = useState(false);
    // const [todoList, setTodoList] = useState([]);
    const titleHandler = (e) => {
        setTitle(e.target.value);
    };
    const descHandler = (e) => {
        setDesc(e.target.value);
    };
    const dispatch = useDispatch();
    const addTodoHandler = () => {
        // setId(crypto.randomUUID());
        // setTodoList([...todoList, { title: title, desc: desc, isDone: isDone, id: id }]);
        dispatch(addTodo({ title: title, desc: desc, isDone: false, id: crypto.randomUUID() }));
        setTitle('');
        setDesc('');
    };

입력하는 컴포넌트에서 기존에 있던 필요없는 state는 주석처리하고 dispatch를 통해 리듀서로 이동되도록 하였다.

 

const todoState = (state = initialState, action) => {
    switch (action.type) {
        case ADD_TODO: {
            console.log(action.payload);
            return {
                todo: [...state.todo, action.payload],
            };
        }
        case DELETE_TODO: {
            return {};
        }
        case TOGGLE_TODO: {
            return {};
        }
        default:
            return state;
    }
};

ADD_TODO리듀서에서 action.payload를 받으면, 기존 state.todo를 스프레드 연산자로 펼쳐주고, action.payload를 뒤에 붙이는 형식이다.

 

const TodoList = () => {
    const todoBox = {
        display: 'flex',
        flexDirection: 'row',
        flexWrap: 'wrap',
        gap: '10px',
    };
    const todoList = useSelector((state) => state.todoState.todo);

    return (
        <div>
            <Title>Working... 🔥</Title>
            <div style={todoBox}>
                {todoList.map((todo) => (todo.isDone === false ? <Todo todo={todo} key={todo.id} /> : null))}
            </div>
            <Title>Done..! 🎉</Title>
            <div style={todoBox}>
                {todoList.map((todo) => (todo.isDone === true ? <Todo todo={todo} key={todo.id} /> : null))}
            </div>
        </div>
    );
};

그렇게 저장된 todo들은 TodoList컴포넌트에서 useSelector으로 가져온 것이 todoList에 담겨 map메서드를 통해 todo컴포넌트로 뿌려지게 된다.

addTodo 적용

 

 

다음은 delete와 toggle 기능 구현이다. 해당 기능은 id값을 payload로 dispatch하여 리듀서에서 같은 id값을 가진 todo의 isDone값을 바꿔주거나 삭제하는 방법으로 만들 것이다.

//Todo.jsx
import React from 'react';
import CustomButton from './CustomButton';
import { useDispatch } from 'react-redux';
import { deleteTodo, toggleTodo } from '../../redux/modules/todo';

const Todo = ({ todo }) => {
	//길이가 길어 스타일객체는 지워둠
    const dispatch = useDispatch();
    const deleteHandler = (id) => {
        dispatch(deleteTodo(id));
    };
    const toggleHandler = (id) => {
        dispatch(toggleTodo(id));
    };

    return (
        <div style={todoWrap}>
            <h2>{todo.title}</h2>
            <p>{todo.desc}</p>
            <div style={btnWrap}>
                <CustomButton
                    customStyle={deleteStyle}
                    onClick={() => {
                        deleteHandler(todo.id);
                    }}
                >
                    삭제하기
                </CustomButton>
                {todo.isDone ? (
                    <CustomButton
                        customStyle={cancelStyle}
                        onClick={() => {
                            toggleHandler(todo.id);
                        }}
                    >
                        취소
                    </CustomButton>
                ) : (
                    <CustomButton
                        customStyle={completeStyle}
                        onClick={() => {
                            toggleHandler(todo.id);
                        }}
                    >
                        완료
                    </CustomButton>
                )}
            </div>
        </div>
    );
};

export default Todo;

버튼 클릭 시 알맞은 Handler가 작동하여 리듀서로 todo의 id가 dispatch된다.

 

먼저, 제거기능의 리듀서부터 보자.

//Reducer
const todoState = (state = initialState, action) => {
    switch (action.type) {
        case ADD_TODO: {
            console.log(action.payload);
            return {
                todo: [...state.todo, action.payload],
            };
        }
        case DELETE_TODO: {
            let newTodos = state.todo.filter((el) => el.id !== action.payload);
            return { todo: newTodos };
        }
        case TOGGLE_TODO: {
            return {};
        }
        default:
            return state;
    }
};

state의 todo에서 filter하는데, action.payload에 담겨진 id값과 같지 않은 todo들만 남기는 방식이다.

제거기능 구현

 

다음으로 완료/취소 기능 구현이다.

//Reducer
const todoState = (state = initialState, action) => {
    switch (action.type) {
        case ADD_TODO: {
            console.log(action.payload);
            return {
                todo: [...state.todo, action.payload],
            };
        }
        case DELETE_TODO: {
            let newTodos = state.todo.filter((el) => el.id !== action.payload);
            return { todo: newTodos };
        }
        case TOGGLE_TODO: {
            let targetIndex = state.todo.findIndex((el) => el.id === action.payload);
            let newTodo = [...state.todo];
            if (targetIndex !== -1) {
                newTodo[targetIndex].isDone
                    ? (newTodo[targetIndex] = { ...newTodo[targetIndex], isDone: false })
                    : (newTodo[targetIndex] = { ...newTodo[targetIndex], isDone: true });
            }
            return { todo: newTodo };
        }
        default:
            return state;
    }
};

payload로 받아온 id값과 같은 id값을 가진 todo의 index를 state에서 찾는다.

찾은 index가 존재할 때, 해당 index의 isDone값이 참이라면 거짓으로 바꾸고, 거짓이라면 참으로 바꾸어 state의 todo를 newTodo로 보낸다.

토글기능 구현

 


Trouble Shooting

 

위에 토글기능 구현 사진을 보면 알겠지만, input칸에 무언가를 입력할 때 TodoList컴포넌트까지 리렌더링이 발생하는 것을 알 수 있다.

전역상태관리 라이브러리를 사용하지 않을 때, 컴포넌트끼리 부모자식관계로 연결되어있어야만 state를 자식컴포넌트에서 변경요청할 수 있어 발생했던 문제이다.

전역상태관리 이전 코드

이렇게 TodoList에 todoList state와 setTodoList를 내려주어 연결돼있기 때문에 리렌더링이 발생하는 것이다.

따라서 전역상태관리 라이브러리를 사용하는 지금은 이렇게 연결되어있을 필요가 없다.

InputBox컴포넌트에서 제거하고, App.jsx파일에 직접 import해 넣어준다.
컴포넌트 분리로 불필요한 리렌더링 제거

InputBox만 리렌더링이 되는 것을 볼 수 있다.