[Redux] 순수 Redux App 만들기(생활코딩)
🧐 서론
앞서 Redux는 리액트뿐만 아니라 모든 환경에서 (심지어 vanila JS에서도) 사용가능한 상태관리 툴이라고 했다.
먼저, Redux의 기본개념들과 활용법을 HTML scriipt 환경에 적용하면서 공부해보고, 추후 react-redux 를 활용해 리액트에서의 활용법을 공부하려고 한다.
💜 Without Redux 💀
이처럼, 버튼을 누르면 박스들의 색깔이 바뀌는 앱을 만든다고 가정하자.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./style.css">
<title>Document</title>
</head>
<body>
<div id="red"></div>
<div id="green"></div>
<div id="blue"></div>
<script>
function red() {
document.querySelector('#red').innerHTML = `
<div class="container" id="component_red">
<h1>red</h1>
<input type="button" value="fire"
onclick="
document.querySelector('#component_red').style.backgroundColor = 'red'
document.querySelector('#component_green').style.backgroundColor = 'red'
document.querySelector('#component_blue').style.backgroundColor = 'red'
// yellow, purple, brown....
"/>
</div>
`;
}
function green() {
document.querySelector('#green').innerHTML = `
<div class="container" id="component_green">
<h1>green</h1>
<input type="button" value="fire"
onclick="
document.querySelector('#component_red').style.backgroundColor = 'green'
document.querySelector('#component_green').style.backgroundColor = 'green'
document.querySelector('#component_blue').style.backgroundColor = 'green'
// yellow, purple, brown....
"/>
</div>
`;
}
function blue() {
document.querySelector('#blue').innerHTML = `
<div class="container" id="component_blue">
<h1>blue</h1>
<input type="button" value="fire"
onclick="
document.querySelector('#component_red').style.backgroundColor = 'blue'
document.querySelector('#component_green').style.backgroundColor = 'blue'
document.querySelector('#component_blue').style.backgroundColor = 'blue'
// yellow, purple, brown....
"/>
</div>
`;
}
red();
green();
blue();
// yellow, purple, brown....
</script>
</body>
</html>
이처럼, 색깔(박스)들이 추가될 때 마다 색깔함수와, 모든 함수의 onclick 인자로 각각 추가를 해줘야한다.
지금은 HTML 안에 모두 들어있지만, 박스 하나하나가 컴포넌트라면 그만큼 수정소요와 props Drilling 추가가 필요할 것이다.
💜 With Redux 😍
- 설치
우선, Redux 라이브러리를 설치해야 한다. 기본적으론, Node.js npm 을 통해 설치가 가능하다.
npm install redux (--save, if 'package.json' exists)
혹은, 이처럼 HTML에서 바로 적용을 하기 위한 방법이 있다. redux-cdn 사이트에서, 링크를 <head>에 첨부하면 된다.
<head>
<!-- ... -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.0-beta.1/redux.js" integrity="sha512-a18iH5k0MJdFLaOvN1STjT2wNpH4tjIKSuWXZOZ7nWxkMydoMGZCkQ88Ch/RnAVzVVNVfhl3dxyg5UKABKhr0Q==" crossorigin="anonymous"></script>
<title>Document</title>
</head>
- 코드수정 (body)
function reducer(state, action) {
let newState;
if (!state) {
return {
color: 'yellow',
}
}
else if (action.type === 'CHANGE_COLOR') {
// Javascript Immunity : 원본을 수정하면 REDO, UNDO 불가
newState = Object.assign({}, state, {color: action.color}) // state 복제
}
return newState;
}
let store = Redux.createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
function red() {
let state = store.getState()
document.querySelector('#red').innerHTML = `
<div class="container" id="component_red" style="background-color: ${state.color}">
<h1>red</h1>
<input type="button" value="fire"
onclick="store.dispatch({type: 'CHANGE_COLOR', color: 'red'})" />
</div>
`;
}
store.subscribe(red);
red();
function blue() {
let state = store.getState()
document.querySelector('#blue').innerHTML = `
<div class="container" id="component_blue" style="background-color: ${state.color}">
<h1>blue</h1>
<input type="button" value="fire"
onclick="store.dispatch({type: 'CHANGE_COLOR', color: 'blue'})" />
</div>
`;
}
store.subscribe(blue);
blue();
function green() {
let state = store.getState()
document.querySelector('#green').innerHTML = `
<div class="container" id="component_green" style="background-color: ${state.color}">
<h1>green</h1>
<input type="button" value="fire"
onclick="store.dispatch({type: 'CHANGE_COLOR', color: 'green'})" />
</div>
`;
}
store.subscribe(green);
green();
- store 변수에 스토어를 만들어줬다. (Redux.createStore()) 이 메서드는, reducer 함수를 인자로 받아야 한다.
- reducer 함수를 선언한다. 인자는, (state, action)을 받으며, action.type에 따라 state 수정로직이 달라진다. (본래는 switch문)
- reducer 함수의 중점은, state 자체를 수정하는 것이 아닌 newState라는 새로운 상태를 덮어씌운다.(Object.assign() 메서드) 원본을 수정하면, 이전상태 복원이나 Redux DevTools의 시간여행 등이 불가하기 때문이다.
- 각각의 색깔 함수들은 스토어의 state를 가져온다. (store.getState()) 여기의 color 값을 배경색으로 설정한다.
- 또, 버튼 onclick 이벤트 시 디스패치를 실행한다. (store.dispatch()) 인자로 action 객체에 type과 변경할 color를 넣어준다.
- 모든 함수들은 구독을 겸한다. (store.subscribe()) state가 바뀌면, 해당 함수들이 실행되면서 배경색이 최신화된다.
💜 Pure Redux App
이제, 생활코딩 형님과 함께 위 Pure Redux 앱을 만드려고 한다. CRUD의 update를 제외한 기능들을 Redux로 만들었다.
전체 코드는 길기 때문에, Redux 구성(store, reducer)이나 컴포넌트의 기능들을 각각 보도록 하자.
1. 스토어, 리듀서 함수 설정
const reducer = (state, action) => {
if (!state) {
return {
mode:'welcome',
selectedId: 1,
contents: [
{
id: 1,
title: 'HTML',
desc: 'HTML is...'
},
{
id: 2,
title: 'CSS',
desc: 'CSS also...'
},
]
}
}
let newState = {};
if (action.type === 'CHANGE') {
Object.assign(newState, state, {mode: action.mode})
}
else if (action.type === 'SELECT') {
Object.assign(newState, state, {mode: action.mode, selectedId: action.id})
}
else if (action.type === 'CREATE') {
const addContent = {id: state.contents.length + 1, title: action.title, desc: action.desc,}
const newContents = state.contents.concat(addContent)
Object.assign(newState, state, {mode: 'read', contents: newContents})
}
else if (action.type === 'DELETE') {
const newId = state.contents[0].id
const newContents = state.contents.filter(item => item.id !== state.selectedId)
Object.assign(newState, state, {selectedId: newId, contents: newContents})
}
return newState;
}
let store = Redux.createStore(reducer);
스토어는 Redux.createStore([리듀서]) 메서드를 통해 생성하여 store 변수에 담는다. (dispatch, getState, subscribe 활용위함)
리듀서 함수 역시, 위처럼 (state, action) 두 인자를 받는 함수로 설정해주면 된다.
- state가 없는 경우(undefined), 기본 state를 세팅할 수 있다.
mode(<article> 디스플레이), selectedId(선택된 컨텐츠), contents(컨텐츠 목록) 상태들을 설정했다.
- 추가로, newState 변수와 action.type 에 따른 수정로직을 각각 작성한다. (불변성을 유지하기 위해, Object.assign() 생성 및 수정)
- CHANGE: mode를 바꾼다. (create <a> 클릭시 모드변경 위함)
- SELECT: mode와 selectedId를 바꾼다. (각 리스트 목록을 클릭시, 해당 목록Id로 최신화하기 위함, 모드는 read)
- CREATE: contents에 새로운 컨텐츠를 추가한다. (submit 시, action에 담긴 title, desc 값으로 추가한다. 불변성을 위해 concat)
- DELETE: selectedId의 컨텐츠를 삭제한 contents로 수정한다. (불변성을 위해 filter)
2. Links 컴포넌트
const Links = () => {
const { contents } = store.getState();
let listTags = '';
for (let item of contents) {
listTags += `
<li>
<a href="${item.id}.html"
onclick="
event.preventDefault();
store.dispatch({
type: 'SELECT',
mode: 'read',
id: ${item.id},
})
"
>${item.title}</a>
</li>
`;
}
document.querySelector('.links').innerHTML = `
<nav>
<ol>${listTags}</ol>
</nav>
`;
}
<nav>와 <ol>을 렌더하고, 안에는 listTags 변수에 담긴 <li>태그들이 포함된다.
각 리스트는 클릭시, 디스패치를 발생시킨다. read 모드와, 해당 리스트의 아이디로 selectedId 를 최신화하며 <article>을 바꾼다.
3. Controls 컴포넌트
const Controls = () => {
document.querySelector('.controls').innerHTML = `
<section>
<a href="/create"
onclick="
event.preventDefault();
store.dispatch({
type: 'CHANGE',
mode: 'create',
})
"
>create</a>
<input type="button" value="delete"
onclick="
store.dispatch({
type: 'DELETE',
})
"
/>
</section>
`;
}
create <a>태그 클릭시 디스패치를 발생하며, 'create' 모드로 바꿔준다. <article>이 컨텐츠를 추가하는 <form>으로 바뀐다.
delete 버튼 역시 디스패치를 발생하고, 'DELETE' 액션을 통해 현재 컨텐츠를 삭제한다.
4. Content 컴포넌트
const Content = () => {
const { mode, selectedId, contents } = store.getState();
if (mode === 'welcome') {
document.querySelector('.content').innerHTML = `
<article>
<h2>Welcome!</h2>
<p>This is Redux!!</p>
</article>
`;
}
else if (mode === 'read') {
let renderItem;
for (let item of contents) {
if (item.id === selectedId) {
renderItem = item;
break;
}
}
document.querySelector('.content').innerHTML = `
<article>
<h2>${renderItem.title}</h2>
<p>${renderItem.desc}</p>
</article>
`;
}
else if (mode === 'create') {
document.querySelector('.content').innerHTML = `
<article>
<form onsubmit="
event.preventDefault();
const title = this.title.value
const desc = this.desc.value
store.dispatch({
type: 'CREATE',
title,
desc,
})
">
<p>
<input type="text" name="title" placeholder="title" />
</p>
<p>
<textarea name="desc" placeholder="description"></textarea>
</p>
<p>
<input type="submit" />
</p>
</form>
</article>
`;
}
}
'content'라고 하는 <article> 태그에는, mode 에 따라 표현되는 내용이 달라진다.
- 'welcome': 기본 형태로 제작했으며, 단순한 제목과 내용이 들어가있다.
- 'read': contents 리스트에서, selectedId 값과 일치하는 아이템을 렌더한다(renderItem).
- 'create': 컨텐츠를 추가하기 위한 <form>이 렌더된다. 작성 후 submit 시 디스패치를 발생하며, 'CREATE' 액션과 함께 입력값을 넘겨준다.
5. 구독 설정
Title();
Links();
Controls();
Content();
store.subscribe(Links);
store.subscribe(Content);
위처럼, 모든 컴포넌트들을 렌더하기 위해 각 함수들이 한번씩 실행된다.
또한, Links와 Content 부분은 state가 바뀔때마다 이를 반영해야 하므로, 구독처리를 한 것이다.(store.subscribe([함수]))
DOM + vanila JS 와 Redux 조합으로 간단한 앱을 만들어보았다. (근데 이제 CR_D 기능이 탑재된..)
프로젝트 규모가 작아서 그런지 Redux를 쓰는 것이 용이한 것 같다.
state를 통합관리할 수도 있고, 무엇보다 생활코딩님 설명대로 내가 코딩하는 현재 기능에만 집중하면 된다. (관련 상태나 변경로직)
물론 이전 프로젝트에서 경험했듯, 많은 상태를 중앙관리를 하는 것이 옳지만은 않으며,
필수적인 상태들만 선별하는 것이 중요할 것 같다.
이를, 다음 React-redux 공부를 통해 React와 Redux의 조합을 경험해보고, 필수적인 상태를 선별하는 기준을 잡아가야겠다.
[출처]
- Redux 공식문서: ko.redux.js.org/basics/
- 생활코딩 Redux 강의: www.youtube.com/watch?v=dRcahbiS5zk&list=PLuHgQVnccGMB-iGMgONoRPArZfjRuRNVc&index=18