TodoList & ReduxToolkit

노성호·2021년 6월 25일
0

react

목록 보기
8/12

Redux

Todo List에 Redux를 적용해서 만들어보고 싶었다.

회사에서 redux를 써야 하는데, redux를 내가 개념도 못잡고 있어서 생존;을 위해서라도 redux를 배워야 했다. 일단 Todo List는 최소한의 기능만 구현하기로 했다.
할일 추가 및 할일 삭제.

text input에 글을 작성하고 제출하면 항목이 추가되고, 체크박스를 클릭하면 항목이 지워진다. 일단 state 관리를 통해 가볍게 구현해보았다. 뭐 쉽게 잘 되었다.
그리고 redux를 이용해서 구현을 해 보았다. 많은 삽질끝에 어찌어찌 만들긴 했다.

Redux Toolkit + Todo List

일단 리덕스 툴킷이라는 존재를 몰라서... 리덕스로만 구현을 하고 있었다. 처음 접하는 개념이라 너무 어렵고, 진행도 안되던 와중에 리덕스 툴킷이라는 것을 발견. 바로 갈아탔다.

리덕스 툴킷은 configureStore, createReducer, createAction, createSlice, createSelector라는 리덕스 상태관리를 쉽게 구현해준다.

일단 구현된 코드부터 리뷰한다.

Redux Toolkit

타입

interface ITodo {
  index: number;
  content: string;
}

interface StoreState {
  todos: ITodo[];
}

store와 todo에서 사용할 타입을 정의해준다. ITodo[]만을 이용해서 store의 타입을 정의해줬더니 store 내부에 todo 배열이 있는 형태로 구현이 되었다. 그래서 useSelector()으로 state를 불러올때 todo배열이 아니라 state 오브젝트가 할당되어 직접 배열에 접근하는데 어려움을 겪었다.

const todos = useSelector((state: ITodo[]) => state);

console.log(todos); // {todos: ITodo[]} <= 요렇게

이때 당연히 state는 배열만 리턴할 줄 알았는데, store는 오브젝트 타입으로 리턴되고, reducer에서 사용할 타입은 store의 프로퍼티로 들어간다.

const todos = useSelector((state: any) => state);

useEffect(() => {
  console.log(todos.todos); // [todo, todo]
}, [todos]); 

위와같이 todos를 any타입으로 설정하고 state내부의 todos를 찍어보니 todos 배열이 제대로 출력되었다. 구체적인 타입 설정은 createSlice와 combineReducer에서 더 보기로 한다.

createSlice

export const todoSlice = createSlice({
  name: 'todos',
  initialState: initialState,
  reducers: {
    addTodoList: {
      reducer: (state, { payload }: PayloadAction<{ index: number; content: string }>) => {
        return [...state, { index: payload.index, content: payload.content }];
      },
      prepare: (index: number, content: string) => ({
        payload: {
          index: index + 1,
          content,
        },
      }),
    },
    deleteTodoList: {
      reducer: (state, { payload }: PayloadAction<{ index: number }>) => {
        return [...state.filter((item) => item.index !== payload.index)];
      },
      prepare: (index: number) => ({
        payload: {
          index,
        },
      }),
    },
  },
});

export const { addTodoList, deleteTodoList } = todoSlice.actions;
export const { reducer } = todoSlice;

공식문서 번역의 튜토리얼을 보며 createActoin, createReducer를 직접 구현하면서 따라가니 createSlice 만으로 액션과 리듀서를 간단하게 만들수 있어 createSlice로 리듀서와 액션을 구현하였다.
코드 따라치면서 구현할땐 몰랐는데 트러블 슈팅하는 과정에서 어떻게 돌아가는지 감을 잡은 것 같다.
name항목은 state에 포함되는 키값이 된다. initialState는 todos의 기본값을 셋팅해준다. redusers가 어떤 개념인지 이해하기가 힘들었다.

지금까지 이해하기로는, reducers 내부의 addTodoList와 deleteTodoList는 액션 생성자가 되고, prepare는 액션에서 받은 인자를 payload 객체로 리턴하는 로직이다. reducer는 state와 payload를 인자로 받아 state를 갱신해주는 메서드가 된다.

아랫쪽의 { addTodoList, deleteTodoList } 와 { reducer } 는 구조분해할당으로 todoSlice를 액션과 리듀서로 나눠준다.

PayloadAction
슬라이스에서는 payload 리턴타입을 지정할 수 있다. redux toolkit의 PayloadAction타입을 사용하면 된다고 한다.
참고 링크

prepare
prepare는 payload에 인자가 여러개 들어갈 때 사용된다.
using prepare callbacks to customize action contents

configureStore

const todos = todoSlice.reducer;

const reducers = combineReducers<StoreState>({
  todos,
});

export default function createStore(): Store<StoreState> {
  const store = configureStore({
    reducer: reducers,
  });

  return store;
}

위 코드는 store를 만들어주는 코드다. 타입에서 삽질했던 것 처럼, reducer를 combineReducers로 합쳐주지 않고 당연히 하나니까 이게 스테이트가 되겠지 했었다. combineReducers는 각기 다른 리듀서를 합쳐주는 메서드로, StoreState 타입에 정의된 리듀서들을 리듀서 객체(?) 형태로 합쳐줄 수 있다. 지금은 하나만 필요하니 하나만 합쳐주었다. 이렇게 하면 state.todos로 todos의 배열에 접근할수 있게되고, 구현부에서 state.todos가 ITodo의 배열에 접근할 수 있게된다.
createStore 함수는 StoreState타입을 명시해주고 StoreState타입을 리턴해주는 커스텀 생성자를 만들어주었다.

어떤 프로젝트든 리듀서가 하나는 아니겠지만, 리듀서가 하나만 있을때, 이렇게 combine해주는 것이 좋은 방법인지는 잘 모르겠다.

index.tsx

export const store = createStore();

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root'),
);

여기는 간단하다. store 객체를 만들고 Provider라는 고차 컴포넌트의 인자에 넣어준다.

implement

구현부

function Container() {
  const todos = useSelector((state: StoreState) => state);
  const dispatch = useDispatch();

  function addTodoHandler(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const textInput = event.currentTarget.input.name === 'input' && event.currentTarget.input;
    if (textInput.value === '') return;
    const inputText = textInput.value;
    textInput.value = '';
    dispatch(addTodoList(todos.todos.length, inputText));
  }
  
  function deleteTodo(index: number) {
    dispatch(deleteTodoList(index));
  }

  return (
    <StyledContainer>
      <h1>Todo List</h1>
      <FormWrapper onSubmit={(e) => addTodoHandler(e)}>
        <input name="input" type="text" />
        <input type="submit" />
      </FormWrapper>
      <TodoListBox todos={todos.todos} deleteHandler={deleteTodo}></TodoListBox>
    </StyledContainer>
  );
}

todos는 useSelector를 통해 state를 받아온 객체가 된다. dispatch는 액션 생성자를 이용해 state를 변경해주는 메서드가 된다.
addTodoHandler는 submit이 되었을때, state.todos에 새 todo 목록을 추가해주는 이벤트 핸들러이다. text input의 데이터를 가져와 dispatch와 addTodoList 액션 생성자를 통해 새 할일 목록을 추가해주는 이벤트를 구현했다.
deleteTodo는 체크박스를 클릭했을때 해당 index를 가진 할일 목록을 지워준다.

render

function TodoListBox(props: TodoListProps) {
  if (props.todos.length === undefined) return <></>;
  const todoLists = props.todos.map((item, index) => {
    return (
      <ListWrapper key={index}>
        <input type="checkbox" onChange={(e) => props.deleteHandler(item.index)} />
        <li>{item.content}</li>
      </ListWrapper>
    );
  });

  return <StyledListBox>{todoLists}</StyledListBox>;
}

이건 굳이 설명 안해도 될 것 같다.

결론

redux의 store, action, reducer의 개념을 이해하는데 도움이 많이 되었다.
하지만 redux의 구체적인 구현방식(단방향 데이터 흐름, 구독, 디스패치, 컨트롤러 뷰 등)은 아직 제대로 이해하지 못했다. redux는 지금까지 보지 못했던 패턴이라 이해하기 힘든것 같다. 앞으로 더 공부가 필요하다.

0개의 댓글