[Redux, JS]_todoList

hanseungjune·2023년 3월 11일
0

비전공자의 IT준비

목록 보기
56/68
post-thumbnail

📌 현재 상황

일단 삭제를 했지만, 다른 액션에 문제가 발생한 상황...
그래서 좀 더 잘하는 친구에게 이 에러를 맡기기로 했고...
나는 일단 redux에 대해서 자료를 올려놓고, 저 에러가 해결이 된다면, 그때 다시 수정해서 올리겠다.

👀 에러 해결 사항

기존 코드(에러 발생 부분)

{filteredTodos.map((todo) => (
       <TodoItem
        key={todo.id}
        completed={todo.completed}
        onClick={() => handleTodoClick(todo)}
       >
        <span className='todotext'>{todo.text}</span>
        <span>{todo.completed ? "✅" : "❌"}</span>
        <span onClick={() => handleDeleteClick(todo.id)}>🗑️</span>
       </TodoItem> 
      ))}

수정 코드(에러 발생 부분)

{filteredTodos.map((todo) => (
       <TodoItem
        key={todo.id}
        completed={todo.completed}
        onClick={() => handleTodoClick(todo)}
       >
        <span className='todotext'>{todo.text}</span>
        <span>{todo.completed ? "✅" : "❌"}</span>
        <span onClick={(e) => { e.stopPropagation(); handleDeleteClick(todo.id)}}>🗑️</span>
       </TodoItem> 
      ))}

현재 이벤트 버블링이 걸려있었던 부분이라고 한다. 그래서 (e) => { e.stopPropagation(); } 를 걸어야 이벤트 버블링에서 빠져나올 수 있다고 함.

📌 작성 순서

난 항상 로직도 문제지만, redux를 어떻게 작성순서를 생각해야할까에 대한 고민을 많이했다. redux-saga를 하기 앞서 아직도 고민 중이다. 그래서 글을 올릴꺼다 (... 일단 밥부터 먹고 해볼가 ㅎ )

⭐ index.js(setting)

//index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App />
);

제일 root에 위치한 컴포넌트라고 생각한다.
그래서 세팅의 범위라고 생각하자.

⭐ App.js(setting)

//App.js

import React from 'react';
import styled from '@emotion/styled';
import { Provider } from 'react-redux';
import TodoList from "./TodoList";
import store from './store';

const AppWrapper = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
`

const AppContent = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  border-radius: 5px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
  padding: 20px;
`

const AppTitle = styled.h1`
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 20px;
`

function App() {
  return (
    <Provider store={store}>
      <AppWrapper>
        <AppContent>
          <AppTitle>Todo List</AppTitle>  
          <TodoList />
        </AppContent>  
      </AppWrapper>
    </Provider>
  );
}

해당 컴포넌트는 사실 BrowserRouter, routes, route를 위치시키는 곳이다. 근데 일단 URL을 나눌 필요가 없어서 여기에서 Provider 를 사용했다. ( 사실 여기서 계속 써도 되는지는 나도 잘모름 )

⭐ store.js(setting)

//store.js

import { configureStore } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux";
// 일단 이 파일 없음
import rootReducer from './rootReducer';

const store = configureStore({
  reducer: rootReducer,
});

export const useAppDispatch = () => useDispatch();

export default store;

Redux의 상태를 저장하고 상태가 변경될 때마다 구독된 모든 컴포넌트에 상태 변경 사항을 전달. 그리고 store는 Provider를 통해서 App으로 전달됨

⭐ rootReducer.js(settings)

//rootReducer.js

import { combineReducers } from "redux";
import todosReducer from './todosSlice';
import visibilityFilterReducer from './visibilityFilterSlice';

const rootReducer = combineReducers({
  todos: todosReducer,
  visibilityFilter: visibilityFilterReducer,
})

export default rootReducer;

나는 rootReducer 파일을 만드는것도 일종의 세팅이라고 생각한다. 그래서 나는 미리 만들어 놓을 예정이다.

⭐ components(css) -> createSlice -> reducer 순서로 작성하기

🤞 fetchTodos

// TodoList-components

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
// 이 코드도 문제가 있어보임. => 알고보니 @emotion/react로 설치하고 바꿔야 함
// import { css } from "@emotion/core";
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { fetchTodos } from "./todosSlice";

const TodoListWrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 20px;
`;

const TodoItem = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 300px;
  padding: 10px;
  background-color: #f2f2f2;
  border-radius: 5px;
  margin-bottom: 10px;
  cursor: pointer;
  
  .todotext {
    /* 여기 뭔가 에러 같음 */
    ${({ completed }) =>
      completed &&
      css`
        text-decoration: line-through;
      `}
  }
`;

const TodoList = () => {
  const dispatch = useDispatch();
  const todos = useSelector((state) => state.todos.items);

  useEffect(() => {
    dispatch(fetchTodos())
  }, [dispatch])

  return (
    <TodoListWrapper>
      {todos.map((todo) => (
       <TodoItem
        key={todo.id}
        completed={todo.completed}
        onClick={() => handleTodoClick(todo)}
       >
        <span className='todotext'>{todo.text}</span>
       </TodoItem> 
      ))}
    </TodoListWrapper>
  )
}

export default TodoList;
  • 컴포넌트에서 먼저 작성하는 이유는 일단 어느정도의 틀이 보여야 기능을 구현할 수 있다고 생각해서 그렇다.
  • 물론 map을 이용해서 list를 보기 위해서는 slice(action)과 reducer가 필요하다.
  • 그래서 이 역시도, 다른 자료들을 참고하거나, 마치 이미 기능이 구현되어있다는 것을 가정하면서 코드를 짜야한다.
//todoSlice.js
import { createSlice } from '@reduxjs/toolkit';
import axios from 'axios'

const initialState = {
  items: [],
  isLoading: false,
  error: null,
};

const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    fetchTodosStart: (state) => {
      state.isLoading = true;
      state.error = null;
    },
    fetchTodosSuccess: (state, action) => {
      state.isLoading = false;
      state.items = action.payload;
    },
    fetchTodosFailure: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },
  },
})

export const fetchTodos = () => async (dispatch) => {
  // 원래는 reducers가 actions로 되어있었음 일단 수정해본거
  dispatch(todosSlice.actions.fetchTodosStart());
  try {
    const response = await axios({
      method: 'GET',
      url: 'http://localhost:3001/todos'
    })
    dispatch(todosSlice.actions.fetchTodosSuccess(response.data));
  } catch (error) {
    dispatch(todosSlice.actions.fetchTodosFailure(error.message));
  }
}

// 왜 reducer라고 하는지는 모르겠음
export default todosSlice.reducer;
  • redux-toolkit의 createSlice 라이브러리를 통해서 액션과 액션타입을 굳이 작성하지 않고도, redux를 쓸수 있게 하는 것이다.
  • 일단 내 생각에는 reducer부터 작성하는 게 맞다고 생각한다. 그래서 fetchTodos를 작성한다.
  • try-catch를 통해서 api 호출 및 에러를 구현하고, 그 다음 액션을 호출하는 것을 구현한다. (dispatch)
  • dispatch 인자는 slice에서 가져온것이고, 보통은 'slice.actions.reducer` 형태라고 생각하면 된다.
  • fetch는 Start, Success, Failure
  • slice 작성 시에, state는 initialState를 생각하면서 작성한다
  • 로직은 해당 코드를 참고한다.

🤞 addTodo

//addTodo-component

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
// 이 코드도 문제가 있어보임. => 알고보니 @emotion/react로 설치하고 바꿔야 함
// import { css } from "@emotion/core";
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { fetchTodos, addTodo } from "./todosSlice";

const TodoListWrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 20px;
`;

const TodoForm = styled.form`
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 20px;
`;

const TodoInput = styled.input`
  padding: 10px;
  margin-right: 10px;
  border-radius: 5px;
  border: none;
  outline: none;
`;

const TodoButton = styled.button`
  padding: 10px;
  background-color: #4caf50;
  border: none;
  border-radius: 5px;
  color: #fff;
  cursor: pointer;
`;

const TodoItem = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 300px;
  padding: 10px;
  background-color: #f2f2f2;
  border-radius: 5px;
  margin-bottom: 10px;
  cursor: pointer;
  
  .todotext {
    /* 여기 뭔가 에러 같음 */
    ${({ completed }) =>
      completed &&
      css`
        text-decoration: line-through;
      `}
  }
`;

const TodoList = () => {
  const dispatch = useDispatch();
  const todos = useSelector((state) => state.todos.items);

  useEffect(() => {
    dispatch(fetchTodos())
  }, [dispatch])

  const [todoText, setTodoText] = useState("");

  const handleTodoSubmit = (e) => {
    e.preventDefault();
    if (todoText.trim() !== "") {
      dispatch(addTodo(todoText.trim()));
      setTodoText("");
    }
  }

  return (
    <TodoListWrapper>
      <TodoForm onSubmit={handleTodoSubmit}>
        <TodoInput
          type="text"
          placeholder='새로운 할일을 추가해줘'
          value={todoText}
          onChange={(e) => setTodoText(e.target.value)}
        />
        <TodoButton type='submit'>추가</TodoButton>
      </TodoForm>
      {todos.map((todo) => (
       <TodoItem
        key={todo.id}
        completed={todo.completed}
       >
        <span className='todotext'>{todo.text}</span>
       </TodoItem> 
      ))}
    </TodoListWrapper>
  )
}

export default TodoList;
  • 컴포넌트에서 먼저 작성하는 이유는 일단 어느정도의 틀이 보여야 기능을 구현할 수 있다고 생각해서 그렇다.
  • 물론 map을 이용해서 list를 보기 위해서는 slice(action)과 reducer가 필요하다.
  • 그래서 이 역시도, 다른 자료들을 참고하거나, 마치 이미 기능이 구현되어있다는 것을 가정하면서 코드를 짜야한다.
  • 항상 어떤 데이터를 post를 통해서 추가해야할 때는, form 태그를 써야한다고 생각하는게 좋다.
  • 로직은 위의 코드를 참고하자
//addTodo-slice,reducer
import { createSlice } from '@reduxjs/toolkit';
import axios from 'axios'

const initialState = {
  items: [],
  isLoading: false,
  error: null,
};

const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    fetchTodosStart: (state) => {
      state.isLoading = true;
      state.error = null;
    },
    fetchTodosSuccess: (state, action) => {
      state.isLoading = false;
      state.items = action.payload;
    },
    fetchTodosFailure: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },
    addTodoSuccess: (state, action) => {
      state.items.push(action.payload);
    },
  },
})

export const fetchTodos = () => async (dispatch) => {
  // 원래는 reducers가 actions로 되어있었음 일단 수정해본거
  dispatch(todosSlice.actions.fetchTodosStart());
  try {
    const response = await axios({
      method: 'GET',
      url: 'http://localhost:3001/todos'
    })
    dispatch(todosSlice.actions.fetchTodosSuccess(response.data));
  } catch (error) {
    dispatch(todosSlice.actions.fetchTodosFailure(error.message));
  }
}

export const addTodo = (text) => async (dispatch) => {
  try {
    const response = await axios({
      method: 'POST',
      url: 'http://localhost:3001/todos',
      data: {
        text,
        completed: false,
      }
    });
    dispatch(todosSlice.actions.addTodoSuccess(response.data));
  } catch (error) {
    console.log(error.message)
  }
};

// 왜 reducer라고 하는지는 모르겠음 (그냥 약속인듯)
export default todosSlice.reducer;
  • redux-toolkit의 createSlice 라이브러리를 통해서 액션과 액션타입을 굳이 작성하지 않고도, redux를 쓸수 있게 하는 것이다.
  • 일단 내 생각에는 reducer부터 작성하는 게 맞다고 생각한다. 그래서 addTodos를 작성한다.
  • try-catch를 통해서 api 호출 및 에러를 구현하고, 그 다음 액션을 호출하는 것을 구현한다. (dispatch)
  • dispatch 인자는 slice에서 가져온것이고, 보통은 'slice.actions.reducer` 형태라고 생각하면 된다.
  • add는 Success
  • slice 작성 시에, state는 initialState를 생각하면서 작성한다
  • 로직은 해당 코드를 참고한다.

🤞 toggleTodo

//toggleTodo-component
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
// 이 코드도 문제가 있어보임. => 알고보니 @emotion/react로 설치하고 바꿔야 함
// import { css } from "@emotion/core";
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { fetchTodos, addTodo, toggleTodo} from "./todosSlice";
import FilterLink from './FilterLink';

const TodoListWrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 20px;
`;

const TodoForm = styled.form`
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 20px;
`;

const TodoInput = styled.input`
  padding: 10px;
  margin-right: 10px;
  border-radius: 5px;
  border: none;
  outline: none;
`;

const TodoButton = styled.button`
  padding: 10px;
  background-color: #4caf50;
  border: none;
  border-radius: 5px;
  color: #fff;
  cursor: pointer;
`;

const TodoItem = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 300px;
  padding: 10px;
  background-color: #f2f2f2;
  border-radius: 5px;
  margin-bottom: 10px;
  cursor: pointer;
  
  .todotext {
    /* 여기 뭔가 에러 같음 */
    ${({ completed }) =>
      completed &&
      css`
        text-decoration: line-through;
      `}
  }
`;

const TodoList = () => {
  const dispatch = useDispatch();
  const todos = useSelector((state) => state.todos.items);

  useEffect(() => {
    dispatch(fetchTodos())
  }, [dispatch])

  const [todoText, setTodoText] = useState("");

  const handleTodoSubmit = (e) => {
    e.preventDefault();
    if (todoText.trim() !== "") {
      dispatch(addTodo(todoText.trim()));
      setTodoText("");
    }
  }

  const handleTodoClick = (todo) => {
    dispatch(toggleTodo(todo));
  };

  return (
    <TodoListWrapper>
      <TodoForm onSubmit={handleTodoSubmit}>
        <TodoInput
          type="text"
          placeholder='새로운 할일을 추가해줘'
          value={todoText}
          onChange={(e) => setTodoText(e.target.value)}
        />
        <TodoButton type='submit'>추가</TodoButton>
      </TodoForm>
      {todos.map((todo) => (
       <TodoItem
        key={todo.id}
        completed={todo.completed}
        onClick={() => handleTodoClick(todo)}
       >
        <span className='todotext'>{todo.text}</span>
        <span>{todo.completed ? "✅" : "❌"}</span>
       </TodoItem> 
      ))}
    </TodoListWrapper>
  )
}

export default TodoList;
  • 일단 todo.completed가 true인지, false인지에 따라서 아이콘이 바뀐다. 그리고 추가적으로 todo.text에 취소선이 생긴다(true).
import { createSlice } from '@reduxjs/toolkit';
import axios from 'axios'

const initialState = {
  items: [],
  isLoading: false,
  error: null,
};

const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    fetchTodosStart: (state) => {
      state.isLoading = true;
      state.error = null;
    },
    fetchTodosSuccess: (state, action) => {
      state.isLoading = false;
      state.items = action.payload;
    },
    fetchTodosFailure: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },
    addTodoSuccess: (state, action) => {
      state.items.push(action.payload);
    },
    toggleTodoSuccess: (state, action) => {
      const todo = state.items.find((todo) => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
  },
})

export const fetchTodos = () => async (dispatch) => {
  // 원래는 reducers가 actions로 되어있었음 일단 수정해본거
  dispatch(todosSlice.actions.fetchTodosStart());
  try {
    const response = await axios({
      method: 'GET',
      url: 'http://localhost:3001/todos'
    })
    dispatch(todosSlice.actions.fetchTodosSuccess(response.data));
  } catch (error) {
    dispatch(todosSlice.actions.fetchTodosFailure(error.message));
  }
}

export const addTodo = (text) => async (dispatch) => {
  try {
    const response = await axios({
      method: 'POST',
      url: 'http://localhost:3001/todos',
      data: {
        text,
        completed: false,
      }
    });
    dispatch(todosSlice.actions.addTodoSuccess(response.data));
  } catch (error) {
    console.log(error.message)
  }
};

export const toggleTodo = (todo) => async (dispatch) => {
  try {
    await axios({
      method: 'PUT',
      url: `http://localhost:3001/todos/${todo.id}`,
      data: {
        completed: !todo.completed,
      },
    });
    dispatch(todosSlice.actions.toggleTodoSuccess(todo.id));
  } catch (error) {
    console.log(error.message);
  }
};

// 왜 reducer라고 하는지는 모르겠음
export default todosSlice.reducer;

사실 삭제하면 해당 toggleTodo에서 에러가 발생하는데, 이걸 해결해야할 것 같다. 친구한테 맡겨놨으니... 해결하면 해당글의 일부를 수정할것 같다.

🤞 deleteTodo

import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
// 이 코드도 문제가 있어보임. => 알고보니 @emotion/react로 설치하고 바꿔야 함
// import { css } from "@emotion/core";
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { fetchTodos, addTodo, toggleTodo, deleteTodo} from "./todosSlice";

const TodoListWrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 20px;
`;

const TodoForm = styled.form`
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 20px;
`;

const TodoInput = styled.input`
  padding: 10px;
  margin-right: 10px;
  border-radius: 5px;
  border: none;
  outline: none;
`;

const TodoButton = styled.button`
  padding: 10px;
  background-color: #4caf50;
  border: none;
  border-radius: 5px;
  color: #fff;
  cursor: pointer;
`;

const TodoItem = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 300px;
  padding: 10px;
  background-color: #f2f2f2;
  border-radius: 5px;
  margin-bottom: 10px;
  cursor: pointer;
  
  .todotext {
    /* 여기 뭔가 에러 같음 */
    ${({ completed }) =>
      completed &&
      css`
        text-decoration: line-through;
      `}
  }
`;

const TodoList = () => {
  const dispatch = useDispatch();
  const todos = useSelector((state) => state.todos.items);

  useEffect(() => {
    dispatch(fetchTodos())
  }, [dispatch])

  const [todoText, setTodoText] = useState("");

  const handleTodoSubmit = (e) => {
    e.preventDefault();
    if (todoText.trim() !== "") {
      dispatch(addTodo(todoText.trim()));
      setTodoText("");
    }
  }

  const handleTodoClick = (todo) => {
    dispatch(toggleTodo(todo));
  };

  const handleDeleteClick = (id) => {
    dispatch(deleteTodo(id));
  };

  return (
    <TodoListWrapper>
      <TodoForm onSubmit={handleTodoSubmit}>
        <TodoInput
          type="text"
          placeholder='새로운 할일을 추가해줘'
          value={todoText}
          onChange={(e) => setTodoText(e.target.value)}
        />
        <TodoButton type='submit'>추가</TodoButton>
      </TodoForm>
      {filteredTodos.map((todo) => (
       <TodoItem
        key={todo.id}
        completed={todo.completed}
        onClick={() => handleTodoClick(todo)}
       >
        <span className='todotext'>{todo.text}</span>
        <span>{todo.completed ? "✅" : "❌"}</span>
        <span onClick={() => handleDeleteClick(todo.id)}>🗑️</span>
       </TodoItem> 
      ))}
    </TodoListWrapper>
  )
}

export default TodoList;
  • 휴지통을 클릭하면 해당 id를 가진 todo가 지워진다. 어찌 생각해보면 reducer랑 액션은 크게 다르지 않은것 같다.
import { createSlice } from '@reduxjs/toolkit';
import axios from 'axios'

const initialState = {
  items: [],
  isLoading: false,
  error: null,
};

const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    fetchTodosStart: (state) => {
      state.isLoading = true;
      state.error = null;
    },
    fetchTodosSuccess: (state, action) => {
      state.isLoading = false;
      state.items = action.payload;
    },
    fetchTodosFailure: (state, action) => {
      state.isLoading = false;
      state.error = action.payload;
    },
    addTodoSuccess: (state, action) => {
      state.items.push(action.payload);
    },
    toggleTodoSuccess: (state, action) => {
      const todo = state.items.find((todo) => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    deleteTodoSuccess: (state, action) => {
      state.items = state.items.filter((todo) => todo.id !== action.payload);
    },
  },
})

export const fetchTodos = () => async (dispatch) => {
  // 원래는 reducers가 actions로 되어있었음 일단 수정해본거
  dispatch(todosSlice.actions.fetchTodosStart());
  try {
    const response = await axios({
      method: 'GET',
      url: 'http://localhost:3001/todos'
    })
    dispatch(todosSlice.actions.fetchTodosSuccess(response.data));
  } catch (error) {
    dispatch(todosSlice.actions.fetchTodosFailure(error.message));
  }
}

export const addTodo = (text) => async (dispatch) => {
  try {
    const response = await axios({
      method: 'POST',
      url: 'http://localhost:3001/todos',
      data: {
        text,
        completed: false,
      }
    });
    dispatch(todosSlice.actions.addTodoSuccess(response.data));
  } catch (error) {
    console.log(error.message)
  }
};

export const toggleTodo = (todo) => async (dispatch) => {
  try {
    await axios({
      method: 'PUT',
      url: `http://localhost:3001/todos/${todo.id}`,
      data: {
        completed: !todo.completed,
      },
    });
    dispatch(todosSlice.actions.toggleTodoSuccess(todo.id));
  } catch (error) {
    console.log(error.message);
  }
};

export const deleteTodo = (id) => async (dispatch) => {
  try {
    await axios({
      method: 'DELETE',
      url: `http://localhost:3001/todos/${id}`
    });
    dispatch(todosSlice.actions.deleteTodoSuccess(id))
  } catch (error) {
    console.log(error.message)
  }
};

// 왜 reducer라고 하는지는 모르겠음
export default todosSlice.reducer;

toggle의 원리는 특정 id만을 찾아야하고, delete는 특정 id 빼고 다 화면에 보여주면 된다. 그 로직을 기억하자. ( 몰라도 다시 와서 보면됨 )

import React from 'react';
import styled from "@emotion/styled";
import { useSelector, useDispatch } from "react-redux";
// 이 파일 없음 => 다시 만듬
import { setVisibilityFilter } from './visibilityFilterSlice';

const Link = styled.span`
  padding: 5px;
  cursor: pointer;
  /* 이것도 에러같음 */
  text-decoration: ${({ active }) => (active ? "underline" : "none")};
`

const FilterLink = ({filter, children}) => {
  const dispatch = useDispatch();
  const activeFilter = useSelector((state) => state.visibilityFilter);
  const isActive = activeFilter === filter;

  return (
    <Link
      active={isActive}
      onClick={() => dispatch(setVisibilityFilter(filter))}
    >
      {children}
    </Link>
  );
};

export default FilterLink;
  • 사실 여기서는 children 인자가 필요없었는데, 일단은 나중에 쓸일이 있지않을까 해서 넣어놨다.
  • 일단 실행할 필터를 가지고와서 실행할 필터와 내가 클릭한 필터가 동일하다면, 해당 링크로 가서 화면을 보여준다. (필터를 거는 방식)
import { createSlice } from "@reduxjs/toolkit";

const visibilityFilterSlice = createSlice({
  name: "visibilityFilter",
  initialState: "SHOW_ALL",
  reducers: {
    setVisibilityFilter: (state, action) => action.payload,
  }
})

export const { setVisibilityFilter } = visibilityFilterSlice.actions;

export default visibilityFilterSlice.reducer;
  • 여기도 slice와 reducer가 있는데, slice에서는 initialState가 약간 액션 타입 지정하는 느낌으로 걸려있다. 왜냐하면 todoList 메인 화면에서 case 별로 필터를 거는 것이 다르기 때문이다. 일단은 모든 todolist를 보여주는 'SHOW_ALL'로 시작한다.
  • api 요청을 제외하고, reducers 속성에는 setVisibilityFilter라는 reducer 함수가 정의되어 있습니다. 이 함수는 인자로 받은 action.payload 값으로 state를 업데이트합니다. setVisibilityFilter 액션은 visibility filter를 설정할 때 사용됩니다.
  • 마지막으로, visibilityFilterSlice.actions 객체에서는 setVisibilityFilter 액션을 가져올 수 있습니다. 사실 로직을 이해하기는 어려워서 만약에 필터를 적용하는 화면을 구현한다면, 해당 포스팅으로 자주 올듯 하다.
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
// 이 코드도 문제가 있어보임. => 알고보니 @emotion/react로 설치하고 바꿔야 함
// import { css } from "@emotion/core";
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { fetchTodos, addTodo, toggleTodo, deleteTodo} from "./todosSlice";
import FilterLink from './FilterLink';

const TodoListWrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 20px;
`;

const TodoForm = styled.form`
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 20px;
`;

const TodoInput = styled.input`
  padding: 10px;
  margin-right: 10px;
  border-radius: 5px;
  border: none;
  outline: none;
`;

const TodoButton = styled.button`
  padding: 10px;
  background-color: #4caf50;
  border: none;
  border-radius: 5px;
  color: #fff;
  cursor: pointer;
`;

const TodoItem = styled.div`
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 300px;
  padding: 10px;
  background-color: #f2f2f2;
  border-radius: 5px;
  margin-bottom: 10px;
  cursor: pointer;
  
  .todotext {
    /* 여기 뭔가 에러 같음 */
    ${({ completed }) =>
      completed &&
      css`
        text-decoration: line-through;
      `}
  }
`;

const FilterWrapper = styled.div`
  margin-top: 10px;
`;

const TodoList = () => {
  const dispatch = useDispatch();
  const todos = useSelector((state) => state.todos.items);
  const visibilityFilter = useSelector((state) => state.visibilityFilter);

  useEffect(() => {
    dispatch(fetchTodos())
  }, [dispatch])

  const [todoText, setTodoText] = useState("");

  const handleTodoSubmit = (e) => {
    e.preventDefault();
    if (todoText.trim() !== "") {
      dispatch(addTodo(todoText.trim()));
      setTodoText("");
    }
  }

  const handleTodoClick = (todo) => {
    dispatch(toggleTodo(todo));
  };

  const handleDeleteClick = (id) => {
    dispatch(deleteTodo(id));
  };

  const filteredTodos = todos.filter((todo) => {
    switch (visibilityFilter) {
      case "SHOW_ALL":
        return true;
      case "SHOW_COMPLETED":
        return todo.completed;
      case "SHOW_ACTIVE":
        return !todo.completed;
      default:
        return true;
    }
  });

  return (
    <TodoListWrapper>
      <TodoForm onSubmit={handleTodoSubmit}>
        <TodoInput
          type="text"
          placeholder='새로운 할일을 추가해줘'
          value={todoText}
          onChange={(e) => setTodoText(e.target.value)}
        />
        <TodoButton type='submit'>추가</TodoButton>
      </TodoForm>
      {filteredTodos.map((todo) => (
       <TodoItem
        key={todo.id}
        completed={todo.completed}
        onClick={() => handleTodoClick(todo)}
       >
        <span className='todotext'>{todo.text}</span>
        <span>{todo.completed ? "✅" : "❌"}</span>
        <span onClick={() => handleDeleteClick(todo.id)}>🗑️</span>
       </TodoItem> 
      ))}
      <FilterWrapper>
        <FilterLink filter="SHOW_ALL">모든 리스트</FilterLink>
        <FilterLink filter="SHOW_ACTIVE">진행중인 리스트</FilterLink>
        <FilterLink filter="SHOW_COMPLETED">완료된 리스트</FilterLink>
      </FilterWrapper>
    </TodoListWrapper>
  )
}

export default TodoList;
  • todos를 filteredTodos로 바꾸었다. name으로만 가지고오면, state 자체를 가지고 오는 것이다. 참고하여 case를 구분해준다.
  • FilterLink 컴포넌트는 버튼이라고 생각하고 filter를 지정해주는 식으로 filter의 타입을 지정해준다. 그래야 관련 액션이 호출되면서, 필터가 걸린다.

역시 나는 이해를 못했기 때문에 다시 찾아와야지 헤헿

📌 결론

역시 나는 개발에 재능이 없다. 하지만 그래도 밥은 벌어 먹고 살아야되니까 그래도 열심히 해야겠다. ㅎㅅㅎ 어딘가는 나를 좋아해주는 곳이 있겠지 히힣

profile
필요하다면 공부하는 개발자, 한승준

0개의 댓글