오늘은 React-Redux에서 hooks를 사용하는 방법을 알아보도록 하겠습니다.
hook은 클래스형 컴포넌트에서 state관리를 함수형 컴포넌트에서 state관리를 할 수 있게 해준 아주 좋은 애입니다.
우리가 전 시간에 react-redux를 배웠을 때 redux에 연결하려고 connect()라는 함수를 사용했었습니다.
간단하게 connect(1,2)(3)으로 되어있으면 1번은 redux state값이 변경될때,
2번은 사용할 이벤트 3번은 1,2번을 가지고 갈 컴포넌트입니다.
자세한 설명은 [React] 15. React-Redux 예제 (Connect) 에서 확인하시면 됩니다.
자 이 connect()라는 함수는 또 함수형 컴포넌트에서는 hooks로 대체합니다.
react-redux 에서는 useSelector와 useDispatch등 내장함수를 hooks를 통해 제공을 해줍니다.
여기에서는 자주 사용하는 useSelector와 useDispatch를 보겠습니다.
아 그리고 useActions 랑 useRedux 가 사라졌으니 useAction, useRedux를 사용하는 예제를 볼 경우 다른 예제를 보는게 좋습니다.
useSelector 의 경우 connect()함수의 1번과 유사한 기능이며, store의 state의 데이터를 할당할 수 있도록 하는 function입니다. 해당 useSelector의 경우는 연결된 action이 dispatch 될때마다, useSelector에 접근되어 값을 반환하게 됩니다. 즉 리랜더링 된다는 말입니다.
useDispatch는 redux store에 설정된 action에 대한 dispatch를 연결하는 hook으로써, store파일 안에 선언된 action을 연결할 수 있도록 선언해줍니다. (관련된 부분은 상단에 props내의 action을 사용할때와 동일합니다.)
이번 예제는 name, age를 입력 받아 저장하면 목록에 나오는 예제를 만들어보겠습니다.
자 일단 index.js-> store 쪽을 만들어 보겠습니다.
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import {createStore} from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; import { Provider } from 'react-redux'; import rootReducers from './ex/modules/combineStore'; const store = createStore(rootReducers, composeWithDevTools()); ReactDOM.render( <React.StrictMode> <Provider store = {store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') ); serviceWorker.unregister();
코드를 보면 createStore()는 뜻 그대로 store를 생성해주는 함수입니다. redux에서 제공해주는거죠
composeWithDevTools()는 개발자도구에서 redux toold을 사용할 수 있게 해줍니다.
store를 만들었으니까 이제 store를 제공을 해줘야겠죠?
<Provider store = {store}>
로 store를 에서 사용할 수 있게 해줍니다.
이러면 store가 생성이되고 제공이 되는겁니다.
이제 store를 제공해줬는데 어떤 store를 제공했는지 보도록 하겠습니다.
import {combineReducers} from 'redux'; import counter from './counter'; import todoListStore from './todoListStore'; const rootReducer = combineReducers({ counter, todoListStore }); export default rootReducer;
rootReducer의 역할은 여러개의 reducer들을 하나로 합쳐주는 역할을 합니다. 그 역할을 하는 애응 combineReducers()입니다.
combineReducers(counter,todoListStore)는 counter라는 리듀서와 todoListStore라는 리듀서를 combineReducers()를 통해 하나의 reducer로 반환해줍니다.
여기서 우리는 todoReducer만 사용하기 때문에 설명을 하기 위해 만든counter는 삭제해야합니다.
이제 우리가 진짜 사용하는 todoListStore라는 리듀서를 보도록 하겠습니다.
import * as actions from './todoListAction'; // 액션 생성 함수 export const save = () => ({type : actions.SAVE}); export const select = (id) => ({type : actions.SELECT, id : id}); export const delete1 = (id) => ({type : actions.DELETE, id : id}); export const change = (changeData) => ({type : actions.CHANGE, info : changeData}); // state 초기값 const init = { index : 2, info :[ {id : 1, name : 'ㅎ', age : '10', mode : 'R'}, {id : 2, name : 'S', age : '10', mode : 'R'} ], selectData : { id : '', name : '', age : '', mode : '' } }; // Reducer const todoListStore = (state = init, action) => { switch(action.type){ case actions.SAVE: switch(state.selectData.mode){ case "U" : let upData = Object.assign({},state.selectData); upData.mode = 'R'; upData.id = parseInt(upData.id); let i=0; (state.info).forEach( info => { if(info.id === upData.id){ state.info[i] = upData; } i++; }); return {...state, info : state.info, selectData : upData}; case "I" : debugger; let newData = Object.assign({},state.selectData); newData.id = ++state.index; newData.mode = 'R'; let sumInfo = state.info.concat(newData); state.selectData = { id : '', name : '', age : '', mode : ''}; return {...state, info : sumInfo}; default : return; } return state; case actions.SELECT: debugger; let data1 = (state.info).find(data => { if(data.id === parseInt(action.id)){ return true; } }) return {...state, selectData : data1}; case actions.DELETE: return state; case actions.CHANGE: return {...state, selectData : action.info} default : return state; } } export default todoListStore;
위 코드부터 살펴 보겠습니다.
액션 생성함수는 dispatch()로 액션을 호출해서 return값을 reducer에 넘겨줍니다.
export const save = () => ({type : actions.SAVE}); export const select = (id) => ({type : actions.SELECT, id : id}); export const delete1 = (id) => ({type : actions.DELETE, id : id}); export const change = (changeData) => ({type : actions.CHANGE, info : changeData});
이때 type의 값 actions.*은 액션함수는 todoListAction를 따로 만들어 관리를 했습니다.
import * as actions from './todoListAction';
export const ALLSELECT = 'ex/modules/ALLSELECT'; export const SAVE = 'ex/modules/SAVE'; export const SELECT = 'ex/modules/SELECT'; export const DELETE = 'ex/modules/DELETE'; export const CHANGE = 'ex/modules/CHANGE';
다시 본론으로 넘어와서
const init = { index : 2, info :[ {id : 1, name : 'ㅎ', age : '10', mode : 'R'}, {id : 2, name : 'S', age : '10', mode : 'R'} ], selectData : { id : '', name : '', age : '', mode : '' } };
index : 고유 id값을 만들어주기위해 만들었음
info : 목록의 초기 데이터들
selectData : 선택하거나 변경하거나 새로 등록할 때 값을 가지고 있음
다음으로 리듀서를 보도록 하겠습니다.
리듀서는 인자값으로 현재의 state값과, 행동을 취할 action 생성함수의 return값이 존재합니다.
const todoListStore = (state = init, action) => { }
안의 로직은 switch문을 사용하여 action.type으로 분기를 태우겠습니다.
저장을 하는 부분으로 기존에 있는 데이터를 변경 저장하거나, 새로 저장합니다.
case actions.SAVE: switch(state.selectData.mode){ case "U" : let upData = Object.assign({},state.selectData); upData.mode = 'R'; upData.id = parseInt(upData.id); let i=0; (state.info).forEach( info => { if(info.id === upData.id){ state.info[i] = upData; } i++; }); return {...state, info : state.info, selectData : upData}; case "I" : debugger; let newData = Object.assign({},state.selectData); newData.id = ++state.index; newData.mode = 'R'; let sumInfo = state.info.concat(newData); state.selectData = { id : '', name : '', age : '', mode : ''}; return {...state, info : sumInfo}; default : return; } debugger; return state;
mode로 "U"는 기존 데이터를 변경 저장 "I"는 새로 저장
"U"는 기존 데이터를 찾아서 변경할 데이터로 수정을 합니다.
"I"는 새로운 데이터를 저장하는 거기 때문에 index에 +1를 하여 id값으로 넣어줍니다.
이 case는 목록을 선택했을때 선택된 애의 정보를 가져오기 위함으로 id값으로 find()함수를 사용해 데이터를 selectData에 저장시킵니다.
case actions.SELECT: debugger; let data1 = (state.info).find(data => { if(data.id === parseInt(action.id)){ return true; } }) return {...state, selectData : data1};
이 case는 입력폼에 변경이 일어났을때 변경된 데이터를 selectDAata에 저장시킵니다.
case actions.CHANGE: return {...state, selectData : action.info} }
이렇게 store를 작성했습니다.
이 store를 이제 적용을 시킬 컴포넌트들을 만들어 보겠습니다.
일단 App.js에 root컴포넌트를 만들어주도록하겠습니다.
import ContainerTodoList from './ex/container/ContainerTodoList' export const App = () => { return ( <div> <ContainerTodoList></ContainerTodoList> </div> ) } export default App;
컨테이너 컴포넌트ContainerTodoList를 보도록하겠습니다.
import React from 'react' import { useSelector, useDispatch } from 'react-redux' import TodoList from '../component/TodoList' import * as actions from '../modules/todoListAction' import * as T from '../modules/todoListStore' const ContainerTodoList = () => { const state = useSelector(state => state.todoListStore); const dispatch = useDispatch(); const selectBtn = (id) => {dispatch(T.select(id))}; const selectChange = (changeData) => {dispatch(T.change(changeData))}; const selectSave = ()=> {dispatch(T.save())}; return ( <div> <TodoList selectBtn = {selectBtn} change1 = {selectChange} save1 = {selectSave}></TodoList> </div> ) } export default ContainerTodoList;
를 보면 react-redux의 useSelector()함수와 dispatch()함수를 사용했습니다.
아까 말했듯이 useSelector()는 state를 읽어옵니다.
rootReducer에서 combineReducer에 todoListStore의 state를 읽어옵니다.
useDispatch()함수는 액션생성함수를 호출을 합니다
selectBtn은 목록 선택 selectChange는 입력폼 변경 selectSave는 저장
다음으로 ContainerTodoList에서 하위컴포넌트 TodoList로 가보겠습니다.
import React from 'react' import TodoItem from './TodoItem' import InputForm1 from './InputForm1' import { useSelector, useDispatch } from 'react-redux'; const TodoList = ({selectBtn, change1, save1}) => { let stateData = useSelector(state => state.todoListStore);//state; const listItemClick = (id) =>{ selectBtn(id) } const inputFormChange = (changeData) =>{ change1(changeData); } const selectSave = () => { save1(); } let List = (stateData.info).map((item) => ( <TodoItem item = {item} selectBtn = {listItemClick}></TodoItem> )); return ( <div> <ul> {List} </ul> <div> <InputForm1 inputFormChange = {inputFormChange} selectSave = {selectSave}></InputForm1> </div> </div> ) } export default TodoList;
이 컴포넌트는 목록과 입력폼을 보여주는 컴포넌트입니다.
목록은 map()함수를 사용하여 TodoItem을 부르고, 입력폼은 InputForm1을 부르고 있습니다.
상위 컴포넌트에서 받은 속성 이벤트들은 알맞게 나눠주었습니다.
다음으로 목록 아이템들을 보겠습니다.
import React, {useRef} from 'react' import { useSelector, useDispatch } from 'react-redux'; const TodoItem = ({item, selectBtn}) => { const data = useSelector(state => state.todoListStore); const selectClick = (e) =>{ let initData = data.info; let selectData = data.selectData; let selectInitData = initData.find(data => { if(data.id === parseInt(selectData.id)){ return true; } }); if(JSON.stringify(selectData) === JSON.stringify(selectInitData)){ selectBtn(e.target.dataset.id); } else{ if(selectInitData !== '' && selectInitData !== null && selectInitData !== undefined){ if (window.confirm("수정된 사항이 존재합니다. 계속 진행하시겠습니까?") == true){ selectBtn(e.target.dataset.id); } else{ } } else{ selectBtn(e.target.dataset.id); } } } return ( <div onClick = {selectClick}> <li data-id = {item.id}>id : {item.id} name : {item.name} age : {item.age} mode : {item.mode}</li> </div> ) } export default TodoItem;
상위 컴포넌트에서 받은 item을 가지고 화면에 노출시켜줍니다.
여기서 밸리데이션을 작성하였는데요.
클릭이벤트가 발생 되아 state의 selectData가 존재할 때 선택되었을때의 값과 입력폼의 값이 다르면 해당 window.confirm()창이 뜨도록 한 것입니다.
다음으로 InputForm1.js 를 보겠습니다.
import React, {useState, useRef} from 'react' import { useSelector, useDispatch } from 'react-redux'; import { select } from '../modules/todoListStore'; const InputForm1 = ({inputFormChange, selectSave}) => { const data = useSelector(state => state.todoListStore); const item = data.selectData; const initData = data.info; const [flag, setFlag] = useState(true); const idUseRef = useRef(); const nameUseRef = useRef(); const ageUseRef = useRef(); const modeUseRef = useRef(); const inputInit = () => { setFlag(true); let changeData = { "id" : '', "name" : '', "age" : '', "mode" : '' } inputFormChange(changeData); } const inputChange = (e) => { let id = idUseRef.current.dataset.value; let name = nameUseRef.current.value; let age = ageUseRef.current.value; let mode = modeUseRef.current.dataset.value; let changeData = { "id" : parseInt(id), "name" : name, "age" : age, "mode" : mode }; let selectInitData = initData.find(data => { if(data.id === changeData.id){ return true; } }); if(JSON.stringify(changeData) === JSON.stringify(selectInitData)){ mode = "R"; } else{ if(mode === "R"){ mode = "U"; } else { mode = modeUseRef.current.dataset.value; } } changeData = { "id" : id, "name" : name, "age" : age, "mode" : mode } inputFormChange(changeData); } const inputUpdate =(e) =>{ if(item.id > 0){ setFlag(false); } else{ alert("선택해주세요"); let changeData = { "id" : '', "name" : '', "age" : '', "mode" : '' } setFlag(true); inputFormChange(changeData); } } const inputSave = (e) => { selectSave(); } const inputInsert = (e) => { setFlag(false); let changeData = { "id" : '', "name" : '', "age" : '', "mode" : 'I' } inputFormChange(changeData); } return ( <div> <span ref = {idUseRef} data-value = {data.selectData.id}>id: {data.selectData.id}</span> {' '} name : {' '}<input type = "text" ref = {nameUseRef} disabled = {flag} data-key = "name" value = {data.selectData.name} onChange= {inputChange}></input> age : {' '}<input type = "text" ref = {ageUseRef} disabled = {flag} data-key = "age" value = {data.selectData.age} onChange= {inputChange}></input> <span ref = {modeUseRef} data-value = {data.selectData.mode}>mode : {data.selectData.mode}</span> <button onClick = {inputInsert}>신규</button> <button onClick = {inputUpdate}>수정</button> <button onClick = {inputInit}>초기화</button> <button onClick = {inputSave}>저장</button> </div> ) } export default InputForm1;
간단하게 설명하도록하겠습니다.
신규 버튼: inputInsert 함수에서 입력폼의 데이터를 날려버리고 입력 가능하게 만들어줍니다.
수정 버튼 : inputUpdate 함수에서 선택된 데이터가 있을 경우 수정 가능하게 만들고, 선택된 데이터가 없을 경우 알림창을 띄우고 수정을 못하게 막습니다.
초기화 : inputInit 함수에서 입력폼의 데이터를 리셋시킵니다.
저장 : inputSave 함수에서 상위컴포넌트에서 받은 save 함수를 호출합니다.
이렇게 작성을 하고 실행을 해보면 정상적으로 실행이 됩니다.
버튼 클릭 이벤트 발생했을때 밸리데이션들을 추가해봤는데 추가로 밸리데이션을 해야될 곳들이 있습니다.
한번 해보는 것도 좋습니다.