gatsby로 스타터 프로젝트를 실행하면 페이지가 기본적으로 3개가 있다. 하나는 카운터 기능을 구현한 페이지로 만들었고 메인페이지를 제외하고 남은 하나의 페이지를 할 일 목록 페이지로 만들어 보자.
일단은 최소로 기능할 수 있는 페이지를 구현해 보도록 하자. 페이지가 3개가 있는데 서로의 페이지로 이동이 가능하다. useState로도 구현이 가능하지만 그렇게 되면 다른 페이지로 이동하였을 때 할 일 목록이 전부 사라지게 된다. 그러므로 전체적인 state를 저장할 수 있는 store에 todo list 배열을 만들어서 저장하도록 해보자.
redux / todoDucks
export const ADDITEM = "todoDuck/ADDITEM"
export const addItem = item =>({
type :ADDITEM,
item,
})
const initialState = {
todoItem : [],
}
export default function todoDucks (state = initialState , action){
if(action.type === ADDITEM){
return {
...state,
todoItem : [...state.todoItem , action.item],
}
}else return state
}
보면 addItem
함수는 할 일을(item) 인자로 받아서 액션을 생성하여 리턴한다. todoDucks
리듀서에서는 state의 초기값으로 todoItem
이라는 빈 배열을 받아서 저장한다. 만약 ADDITEM
이라는 액션이 dispatch 되어서 날아오면 todoItem 배열에 item을 추가한다.
redux / todoDucks
import { combineReducers, createStore } from "redux"
import { composeWithDevTools } from "redux-devtools-extension"
import counterReducer from "./reducers/counter"
import todoDucks from "./todoDucks"
const reducer = combineReducers({
counter: counterReducer,
item: todoDucks,
})
const store = createStore(reducer, composeWithDevTools())
export default store
저번에 카운터 페이지에서 생성했던 루트 리듀서에 todoDucks 리듀서를 추가해주자.
redux / todoDucks
TextField from "@material-ui/core/TextField"
import "./Todo.css"
import { useSelector, useDispatch } from "react-redux"
import { addItem } from "../redux/todoDucks"
const Todo = () => {
const [text, setText] = useState("")
const todoItem = useSelector(state => state.item.todoItem)
const dispatch = useDispatch()
//const [todoItem, setTodoItem] = useState([])
const onChangeValue = e => {
setText(e.currentTarget.value)
}
const insertValue = e => {
if (e.key === "Enter") {
dispatch(addItem(text))
//setTodoItem([...todoItem, text]) // todo 리스트에 추가하기
setText("") // 할 일 추가하면 창 비우기
}
}
return (
<div className="whole">
<TextField
id="standard-basic"
placeholder="오늘 할 일은 ?"
onChange={onChangeValue}
onKeyPress={insertValue}
value={text}
/>
<div className="itemContainer">
{todoItem.map(element => (
<div className="item" onClick={e => console.log(element)}>
<li>{element}</li>
</div>
))}
</div>
</div>
)
}
export default Todo
먼저 UI적인 부분에서 보기좋게 material ui
에서 제공하는 Textfield
를 사용하였다 (그냥 input 태그를 사용하여도 된다.)
먼저 text라는 변수와 그것을 수정할 수 있는 setText를 useState로 생성하였다. Textfield 부분에서는 onChange
이벤트에 onChangeValue
함수를 걸어주어서 setText가 실시간으로 입력되는 값을 text에 저장할 수 있게 하였고 이를 통해서 입력창에서 입력되는 값을 볼 수 있게 구현하였다. 그리고 onKeyPress
이벤트를 통해 insertValue
함수를 호출하게 되는데 여기서 key값이 Enter일 경우 현재의 text 값을 액션에 담아서 스토어에 dispatch 하게 된다. 그런 다음 입력창을 비워주기 위해서 setText로 text 값을 clear 해주었다.
이렇게 dispatch된 item들은 store에서 배열의 형태로 저장되므로 map함수를 만들어서 item마다 div태그에 들어갈 수 있게 구현하였다. (클릭 시 어떤 item이 클릭됐는지 알 수 있기 위해서)
redux / todoDucks
import React from "react"
import { Link } from "gatsby"
import "./hello.css"
import Layout from "../components/layout"
import SEO from "../components/seo"
import Todo from "../components/Todo"
import store from "../redux/configureStore"
import { Provider } from "react-redux"
const todo = () => (
<Provider store={store}>
<Layout>
<SEO title="Page two" />
<div>
<Todo />
</div>
<Link to="/">Go back to the homepage</Link>
</Layout>
</Provider>
)
export default todo
페이지에 Todo 컴포넌트를 넣어주었다
src
|-pages
| |_todo.js // todo 페이지
| |_counter.js // 카운터 페이지
| |_index.js // 메인페이지
|
|-components
| |_Counter.js // 카운터 컴포넌트
| |_Todo.js (+ css) // todo 컴포넌트
|
|-redux
|- actions
| |_counter.js
|
|- constants
| |_counter.js
|
|- reducers
| |_counter.js
|
|_ configureStore.js
|
|_ todoDucks.js //todo redux (ducks 패턴)
추가적으로 css 로 가운데 정렬까지 해주고 나면 다음과 같은 결과가 나온다.
카운트와 같이 잘 작동한다.
redux / todoDucks.js
export const ADDITEM = "todoDucks/ADDITEM"
export const DELETEITEM = "todoDucks/DELETEITEM"
export const addItem = item => ({
type: ADDITEM,
item,
})
export const deleteItem = item => ({
type: DELETEITEM,
item,
})
const initialState = {
todoItem: [],
}
export default function todoDucks(state = initialState, action) {
if (action.type === ADDITEM) {
return {
...state,
todoItem: [...state.todoItem, action.item],
}
} else if (action.type === DELETEITEM) {
state.todoItem.splice([...state.todoItem].indexOf(action.item), 1)
return {
...state,
todoItem: [...state.todoItem],
}
} else {
return state
}
}
리듀서 부분만 살펴보면 DELETEITEM 액션이 발생하였을 때 , todoItem 리스트에서 해당 원소를 삭제하고 state의 값을 갱신하는 방식으로 진행하고자 하였다. splice의 반환값은 잘려나간 원소들이므로 먼저 splice로 원소를 빼주고 원래 리스트를 리턴하는 방식으로 구현하였다.
components / Todo.js
...
const deleteValue = e => {
//console.log(e.target.outerHTML)
const newText = e.target.outerHTML.replace(/(<([^>]+)>)/gi, "")
dispatch(deleteItem(newText))
}
return (
<div className="whole">
<TextField
id="standard-basic"
placeholder="오늘 할 일은 ?"
onChange={onChangeValue}
onKeyPress={insertValue}
value={text}
/>
<div className="itemContainer">
{todoItem.map(element => (
<div className="item" onClick={deleteValue}>
<li>{element}</li>
</div>
))}
</div>
</div>
)
}
export default Todo
먼저 리스트 목록중 하나를 클릭할 시 deleteValue 함수를 호출하게 하였다.
deleteValue에서는 해당 원소를 받아서 dispatch 시키려고 하였으나, onClick 이벤트시 deleteValue(element)처럼 데이터를 함께 보내게 되면 렌더링이 엄청나게 많이 발생해서 에러가 발생한다. 그래서 파라미터를 전달하는 방식은 포기하고 어떻게 할까 고민하다가 그냥 html태그를 string 으로 변환해서 정규표현식으로 원소의 값만 빼내는 방식을 채택하였다.
대학교 과제스러운 방식이라 썩 맘에 들지 않아서 다른 방법이 있을까 찾아봤더니 이벤트 버블링이라는 이슈가 있는 것을 확인하였다.
결과는 정상적으로 잘 작동한다.