redux를 사용해 todo 앱 만들어보기.
todo 앱을 redux로 만들어보기 위해 여러 자료들을 찾아보았지만, 너무 내용과 구성이 달라 이해하기 힘들었다 ㅜㅜ
자료들을 보면서 머릿속에 떠돌아다니던 지식들을 합치기 위해서, 간단한 todo앱을 Redux를 사용해 만들어 보았다.
redux의 핵심은:
action
,reducer
,store
이다.
(여기서는 action과 reducer를 같은 파일에 넣어 정리하였다.)
src 폴더
App.js
: TodoList를 컴포넌트로 사용한다.index.js
: 편의를 위해 redux의 store를 여기에 만들어 주었다.src > components 폴더
TodoList.js
: todoItem들을 생성하는 파일TodoItem.js
: input을 통해 입력받은 값을 여기서 출력해준다.src > redux 폴더
index.js
: todos.js 파일과 같은 redux가 많이 생겨나면 rootReudcer를 통해 제어하기 위해 만들어 주었다.todos.js
: TOdoList.js에서 쓰일 action, reducer가 들어있다.프로젝트 시작에 앞서서, 위와 같은 폴더, 파일들을 생성해준다.
cmd를 통해 해당 루트 디렉토리에서 다음과 같은 명령어를 통해 redux 라이브러리를 다운받아 준다.
yarn add redux react-redux
npm install redux react-redux
우선, 먼저 App.js의 코드이다.
import React from 'react'
import TodoList from './components/TodoList';
const App = () => {
return (
<div>
<TodoList />
</div>
)
}
export default App
간단하게 <todoList/>
컴포넌트를 담고있다.
코드를 작성하기에 앞서, todos.js는 redux 파일로, reducer와 action을 담고 있다는 것을 기억할 필요가 있다.
(input를 통해 넣어준 값을 todo 리스트에 저장하기 위해서 다음과 같은 액션 타입을 정의해 준다.)
const ADD_TODO = 'todos/ADD_TODO';
(1에서 작성했던 액션 타입에 어떤 정보가 들어가는지 넣어준다.)
const ADD_TODO = 'todos/ADD_TODO';
let nextId = 1;
export const addTodo = content => ({
type: ADD_TODO,
todo: {
id: nextId++,
content
}
});
어떤 액션 타입을 반환해야 하는지,
어떤 payload를 함께 반환해야 하는지 모두 넣어준다.
const ADD_TODO = 'todos/ADD_TODO';
let nextId = 1;
export const addTodo = content => ({
type: ADD_TODO,
todo: {
id: nextId++,
content
}
});
const todos = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return state.concat(action.todo);
default:
return state;
}
}
export default todos;
ADD_TODO를 가진 액션일 경우, state.concat(action.todo)
를 반환해준다.
이 작업을 통해 기존 todo 리스트에 새로 넣을 값을 concat
(이어 붙이기) 해준다.
주의할 점은 src>index.js 파일과 다른 위치의 index.js 파일이라는 것이다.
굳이 index.js를 사용한 이유는, 해당 파일을 불러올 때, index라는 이름을 가지고 있으면 파일 이름이 아닌, 폴더 이름까지만 경로를 설정하면 되기 때문이다.
import { combineReducers } from "redux";
import todos from './todos';
const rootReducer = combineReducers({
todos,
});
export default rootReducer;
이후 프로젝트 크기를 키워나가다 보면, 여러 reducer들이 생긴다.
이를, CombineReducers를 통해 Reducer들을 하나로 합쳐 관리할 수 있도록 만들어 준다.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import rootReducer from './redux'
import { createStore } from 'redux';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
기존 index.js에서 일부만 수정해주면 된다.
우리가 방금 전에 만들었던 rootReudcer를 가져오고, 이를 이용해 store
를 생성해 준다.
이렇게 생성된 store
는<Provider store ={store}/>
를 통해 react 앱이 store와 연동되게 만들어 준다.
import React, { useState } from 'react';
import TodoItem from './TodoItem';
const TodoList = () => {
const [text, setText] = useState('');
return (
<div>
<form>
<input
value={text}
placeholder="할 일 입력"
/>
<button type="submit">확인</button>
</form>
{todos.map(todo => (
<TodoItem/>
))}
</div>
)
}
export default TodoList;
간단하게 todo 리스트를 입력하는 form(input, button)과 그러한 todo 리스트가 저장되는 <TodoItem/>
를 사용해 코드를 구성했다.
아마 여기까지 코드를 작성하면, todos가 map 되지 않는다는 오류가 뜰 것이다.
이는 아직 todos를 정의하지 않았기 때문인데 이를 redux를 통해 생성해 주고자 한다.
import React, { useState } from 'react';
import { addTodo } from '../redux/todos';
import { useSelector, useDispatch } from 'react-redux';
import TodoItem from './TodoItem';
const TodoList = () => {
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
const onInsert = content => dispatch(addTodo(content))
return (
<div>
<form onSubmit={onSubmit}>
<input
placeholder="할 일 입력."
/>
<button type="submit">확인</button>
</form>
{todos.map(todo => (
<TodoItem/>
))}
</div>
)
}
export default TodoList;
useSelector
: 리덕스의 상태값을 조회하기 위한 hook 함수.
(과거에는 mapStateToProps 를 사용했다.)
useDispatch
: 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해준다.
(과거에는 mapDispatchToProps를 사용했다.)
useSelector
를 통해 리덕스에서 todos를 가져왔다.
이후, action 값(addTodo)를 useDispatch
로 가져왔다.
import React, { useState } from 'react';
import { addTodo } from '../redux/todos';
import { useSelector, useDispatch } from 'react-redux';
import TodoItem from './TodoItem';
const TodoList = () => {
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
const onInsert = content => dispatch(addTodo(content));
const [text, setText] = useState('');
const onChange = e => setText(e.target.value);
const onSubmit = e => {
e.preventDefault();
onInsert(text);
setText('');
};
return (
<div>
<form onSubmit={onSubmit}>
<input
value={text}
placeholder="할 일 입력"
onChange={onChange}
/>
<button type="submit">확인</button>
</form>
{todos.map(todo => (
<TodoItem key={todo.id} content={todo.content} />
))}
</div>
)
}
export default TodoList;
input 태그를 통해 입력값을 받는데 이는 변동이 되므로 useState
를 통해 입력값이 변동이 있을 때마다 setText를 통해 text를 바꿔준다.
onSubmit
이벤트는 버튼으로 input 값을 제출할 때 사용한다.
e.preventDefault()
를 통해 페이지가 새로고침 되는 것을 막았으며,
onInsert의 값으로 text를 넣어 줌으로써, addTodo 액션에 text를 전달해 주었다.
setText('')
를 통해 입력 값으로 가득 차있던 input 부분을 공란으로 비워준다.
마지막으로 TodoItem에 key값과, content를 porps로 전달해 준다.
import React from 'react'
const TodoItem = ({ id, content }) => {
return (
<div>
<h3>({id}) {content}</h3>
</div>
)
}
export default TodoItem
마지막으로 props로 전달한 content 값을 todo 리스트의 항목으로 보여주는 컴포넌트를 작성한다.
만약, 성공적으로 코드를 작성하였으면 (최종 써있는 코드를 복사해서 붙여넣기 해도 실행 가능), 다음과 같은 화면을 볼 수 있다.
참고로, 코드의 간소화를 위해 css는 넣지 않았으며, 원한다면 css를 적용해서 사용할 수 있다.
만약, input에 내용을 작성하고 확인을 누른다면 다음과 같은 화면을 볼 수 있다!!