오늘은 React 에서 Redux 를 사용하는 방법에 대해서 알아보도록 하겠다.
그러기 위해서는 우선 2가지 기술이 선행되어야 한다.
위 내용들을 모른다면 조금 힘들어질 수 있으니 위 링크에서 확인하는 것도 좋은 방법인 것 같다.
목차
- redux와 react-redux의 차이
- redux 의 기본 개념
- 구성 요소
- Provider
useDispatch()
useSelector()
- react-redux 로 todo list 만들기
- 프로젝트 만들기
- 의존성 추가
- styled-components
- react-redux
- UI 작업하기
- 홈 화면 만들기
- 리덕스 작업하기
- action type 만들기
- action creator 만들기
- reducer 만들기
- store 추가하기
redux와 react-redux의 차이
잘 알다싶이 리덕스는 Vue, Angular, Ember, Vanilla JS 와는 별개로 돌아가는 독립적인 Javascript 라이브러리이다.
리덕스는 다른 프레임워크나 라이브러리에서 쓸 때는 UI 바인딩을 해야되는데, 그게 바로 React에서는 React-Redux 라고 불린다.
react-redux, 공식 홈페이지 에서 자세한 컨셉을 확인할 수 있으니 확인하는 것도 좋은 방법인 것 같다.
redux의 기본 개념
redux 의 기본 개념과 구성요소에 대해서는 지난 시간 에서 확인할 수 있지만 간단하게 되짚고 넘어가보자.
구성 요소
- Provider
- action
- 순수 js 객체
- action type
- action payload
- 순수 js 객체
- action Creator
- action 객체를 반환하는 함수
- Dispatch
- action 을 리듀서에게 전달한다
- Reducer
- action 이 (새로운 state를 반환)
- Store
- 관리하려는 state를 저장하는 곳
Provider
Provider 는 리덕스의 가장 기본이라고 할 수 있다.
우리는 Redux 를 state 를 관리하고 Global 하게 사용하기 위해서 사용하는데, 그러기 위해서는 가장 최상위 컴포넌트에 Provider 컴포넌트를 부모 컴포넌트로 만들어야 한다.
보통 create-react-app 을 통해서 리액트 앱을 만들게 되면 App 컴포넌트가 가장 상위 컴포넌트가 된다.
App 컴포넌트 내부에 있는 컴포넌트들이 store를 사용할 수 있게 하기 위해서 <Provider />
로 wrapping 해야 한다.
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
보통 React 에서는 App.js
, Next.js 에서는 _app.js
에서 추가해주면 된다.
useDispatch 와 useSelector
useDispatch()와 useSelector()는 리덕스를 함수형 컴포넌트에서 사용하기 위한 방법이다.
리액트가 Hook을 발표한 이후로 우리는 이제 더 이상 클래스형 컴포넌트를 사용하지 않는다.
클래스형 컴포넌트에서 리덕스를 사용하기 위해서는 connect, mapDispatchToProps, mapStateToProps와 같은 HOC를 이용해야 했지만 이제는 Hook 을 이용해서 더욱 가독성 있고 편리한 리덕스를 사용할 수 있게 된 것이다.
useSelector()
useSelector 는 Redux Store 로 부터 state 를 가져올 때 사용한다.
import { useSelector } from "react-redux";
const result = useSelector((state) => state.someState);
useDispatch()
useDispatch 는 action 을 dispatch 할 때 사용할 수 있는 훅이다.
import { useDispatch } from "react-redux";
const dispatch = useDispatch();
dispatch(ACTION);
useDispatch 에서는 memoize를 위해서 useCallBack() 와 함께 사용하기도 하는데, 자세한 내용은 바로 이 다음 시간에 알아보도록 하자!
이제 실전으로 들어가 todo list 를 만들어보자
react-redux 로 todo list 만들기
Todo List 를 만들 것인데 아마 결과물은 다음과 같이 생겼을 것이다.
순서는 앞선 목차에서 이야기 했던 것 처럼 다음과 같은 순서로 개발 한다.
- 프로젝트 만들기
- 의존성 추가
- 리덕스 작업하기
- UI 작업하기
프로젝트 만들기 & 의존성 추가
CRA 를 이용해서 간단한 리액트 프로젝트를 생성해보자.
$ create-react-app redux-todo
$ npx create-react-app redux-todo
그리고 의존성들을 추가해보자.
간단하게 redux와 styled-component 를 추가해보자.
$ npm install -y styled-components
$ npm install -y redux
이렇게 까지 했다면 package.json
에 의존성이 잘 들어왔는지 확인하고 루트 폴더에서 App.js와 index.js 만 남기고 나머지는 삭제한 뒤, 필요하지 않은 코드들을 지워 실행이 되는 것 까지 확인하자!
리덕스 작업하기
이번 글의 핵심인 리덕스 작업하기 부분이다.
리덕스 작업을 하기 위해서는 3개의 js 파일이 필요하다.
- actions.js
- action 생성을 하고 해당 action 이 어떤 일을 수행할지 지정한다.
- reducer.js
- action 을 실행시킬 reducer
- action 객체를 받아 state 를 변경시킨다.
- store.js
- reducer 로 global store 를 만들 redux store
commons
디렉토리를 생성하고 다음 3개의 파일을 각각 작성하도록 하자.
actions.js
export const ADD = "ADD_TODO";
export const DELETE = "DELETE_TODO";
let id = 1;
export const add_todo = (todo) => {
return {
type: ADD,
todo: {
id: id++,
title: todo.title,
isComplete: todo.isComplete,
},
};
};
export const delete_todo = (id) => {
return {
type: DELETE,
id,
};
};
우리는 이번 todo list 에서 2개의 액션만을 사용할 것이다.
- todo 생성
- todo 삭제
todo를 생성하기 위해서는 매개변수로 받은 todo 객체를 reducer 에게 반환 하고, todo를 삭제하기 위해서는 삭제하려는 todo의 id 만을 넘겨주도록 한다.
자세한 생성, 삭제 로직은 reducer 에게 위임하도록 하는 것이 action의 책임의 마지막이다.
reducer.js
import { ADD, DELETE } from "./actions";
const initialState = {
todos: [],
};
export const reducer = (state = initialState, action) => {
if (action.type === ADD) {
return {
// 만약 다른 state 가 존재한다면 전개 연산 ...state 를 해야함
// 하지만 현재 state 에는 todos 하나 뿐이라 todos 만 반환하면 됨
todos: [...state.todos, action.todo],
};
} else if (action.type === DELETE) {
return {
todos: [...state.todos.filter((todo) => todo.id !== action.id)],
};
} else {
return state;
}
};
action-creator 에게 받은 action.type, action.payload를 가지고 이제 실제로 해당 액션이 어떤 결과를 내어야 하는지를 이 reducer 에서 정의하도록 한다.
생성을 위해서는 initialState 에 존재하는 todo 배열에 새로운 항목을 추가한 새로운 state를 반환하도록 한다.
그리고 삭제에서는 action이 넘겨줄 id를 가지고 Array.filter()
메서드를 이용해서 id가 동일한 todo 객체를 삭제한 todos를 반환하도록 한다.
store.js
import { createStore } from "redux";
import { reducer } from "./reducer";
const store = createStore(reducer);
export default store;
리덕스 스토어에 reducer를 넣어주는 작업을 해당 파일에서 수행한다.
UI 작업하기
UI 는 4개의 컴포넌트를 이용해서 구성할 것이고 style 을 아주 간단하게 적용하려 한다. 각각의 컴포넌트는 고유한 styleㄴ.js 파일을 갖게 된다.
- App.js
- InputForm.js
- TodoItem.js
- TodoList.js
App.js & App.styles.js
// App.js
import React from "react";
import * as S from "./App.styles";
import InputForm from "./components/InputForm";
import TodoList from "./components/TodoList";
function App() {
return (
<S.Container>
<S.Wrapper>
<h1>Redux 로 배우는 Todo List</h1>
<InputForm />
<TodoList />
</S.Wrapper>
</S.Container>
);
}
export default App;
// App.styles.js
import styled from "styled-components";
export const Container = styled.div`
width: 100vw;
height: 200vh;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
background: #e9ecef;
input {
appearance: none;
outline-style: none;
border: none;
}
`;
export const Wrapper = styled.div`
margin-top: 300px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
`;
App.js 는 우리가 만들 InputForm, TodoItem, TodoList를 포함하는 컴포넌트이다.
간단한 스타일을 적용하였지만 만약 styled-component나 css 에 대해서 아직 모른다면 스타일은 그냥 넘어가거나 복사해서 사용해도 좋다.
InputForm.js & InputForm.styles.js
해당 파일에서는 TodoItem 을 생성하는 생성 폼 을 구현할 것이다.
// InputForm.js
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import * as S from "./InputForm.styles";
import { add_todo } from "../commons/actions";
const InputForm = () => {
const dispatch = useDispatch();
const [text, setText] = useState("");
const handleChange = (e) => {
const { value } = e.target;
setText(value);
};
const handleClick = () => {
const todo = {
title: text,
isComplete: false,
};
dispatch(add_todo(todo));
setText("");
};
const handleKeyPress = (e) => {
if (e.key === "Enter") {
handleClick();
}
};
return (
<S.Container>
<S.InputBox
type="text"
placeholder="할 일을 입력하세요!!"
onChange={handleChange}
value={text}
onKeyDown={handleKeyPress}
/>
<S.Button onClick={handleClick}>추가 하기</S.Button>
</S.Container>
);
};
export default InputForm;
// InputForm.styles.js
import styled from "styled-components";
export const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
export const InputBox = styled.input`
width: 295px;
height: 40px;
margin: 10px 5px;
border-radius: 15px;
font-size: 1.2rem;
background: white;
padding: 5px 25px;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.34);
`;
export const Button = styled.button`
width: 100px;
height: 50px;
font-size: 1.2rem;
background: white;
border: none;
border-radius: 15px;
color: #20c997;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.34);
cursor: pointer;
`;
InputForm 에서는 텍스트 필드가 변경될 때, 추가 버튼을 클릭했을 때 state를 조작한다.
여기서 눈여겨 봐야 할 것이 우리가 리덕스를 사용한다고 해서 모든 state를 리덕스로 관리할 필요가 없다.
간단한 state 같은 경우에는 그냥 리액트 훅인 useState() 만 사용해도 충분하고 global 하게 사용될 여지가 있는 todo 는 리덕스로 관리하도록 했다.
TodoItem.js & TodoItem.styles.js
우리가 앞에서 reducer 에서 정의한 state 에 존재하는 todos를 렌더링 할 ui 컴포넌트이다.
// TodoItem.js
import React from "react";
import { useDispatch } from "react-redux";
import * as S from "./TodoItem.styles";
import { delete_todo } from "../commons/actions";
const TodoItem = ({ todo }) => {
const dispatch = useDispatch();
const { id, title, isComplete } = todo;
const handleClick = () => {
dispatch(delete_todo(id));
};
return (
<S.Container>
<S.TextColumn>
<div>
<S.Text>{title}</S.Text>
</div>
<S.X onClick={handleClick}>{isComplete || "X"}</S.X>
</S.TextColumn>
</S.Container>
);
};
export default TodoItem;
// TodoItem.styles.js
import styled from "styled-components";
export const Container = styled.div`
margin: 10px 10px;
padding: 10px 10px;
display: flex;
justify-content: flex-start;
align-items: center;
border-radius: 4px;
`;
export const TextColumn = styled.div`
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
`;
export const Text = styled.span`
margin: 0 8px;
`;
export const X = styled.button`
color: red;
border: none;
background: white;
font-size: 1rem;
cursor: pointer;
`;
여기서는 todo item 을 삭제, 즉 해당 todo를 완료했을 경우를 위해서 DELETE action을 dispatch 한다.
이를 위해서 앞서 배운 useDispatch()를 사용하였다.
TodoList.js & TodoList.styles.js
TodoItem 을 렌더링하는 상위 컴포넌트 격인 TodoList 컴포넌트이다.
// TodoList.js
import React from "react";
import * as S from "./TodoList.styles.js";
import TodoItem from "./TodoItem.js";
import { useSelector } from "react-redux";
const TodoList = () => {
const todos = useSelector((state) => state.todos);
return (
<S.Container>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</S.Container>
);
};
export default TodoList;
// TodoList.styles.js
import styled from "styled-components";
export const Container = styled.div`
width: 400px;
padding: 30px;
display: flex;
flex-direction: column;
background: white;
border-radius: 15px;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.34);
`;
해당 컴포넌트에서는 reducer 에 존재하는 state인 todos를 이용해 TodoItem 을 렌더링할 상위 컴포넌트이다.
useSelector 를 이용해서 state 를 가져온 것을 확인할 수 있다.
이렇게 오늘은 Redux를 이용해서 TodoList를 간단하게 구현해보았다.
앞으로 Redux 관련된 리팩토링이나 추가 개념들의 기본 base가 될 게시물이니 더 알아보고싶다면 해당 카테고리를 참고해도 좋을 것 같다.
전체 실습 코드는 github 에 올라가 있으니 참고하시길 바랍니다!
'📺 Front End > -- react & redux & nextjs' 카테고리의 다른 글
[Next.js] Next.js 프로젝트에서 Storybook으로 TDD 하기 (Feat. Atomic Design Pattern) (2) | 2021.01.18 |
---|---|
[Next.js] _document.js로 레이아웃 템플릿을 사용해서 html 과 body 태그 커스터마이징 하기. (0) | 2021.01.03 |
[Next.js] renderPage 함수로 styled-components 오류 방지하기. (0) | 2021.01.03 |
[styled-components] ThemeProvider에서 MediaQuery를 적용하는 2가지 방법 (2) | 2021.01.02 |
[styled-components] ThemeProvider로 공통 스타일 속성 관리하기. (0) | 2021.01.02 |
댓글0