React는 Virtual DOM, 기본적으로 DOM(html)에서 변화한 부분만 rendering 한다.
※ react는 변경사항을 rendering 한다.
Redux는 data를 store에 저장하고, 이를 reducer를 통해 일괄적으로 관리하고 수정하며 subscribe를 통해 구현한다.
※ redux의 store.subscribe는 reducer가 호출되면서 자동으로 같이 동작한다.
※ redux 상태변화 후 subscribe가 동작하여 변화한 상태변화를 rendering하는 구조(일종의 hook)
Redux와 React를 활용하게 되면
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from "./components/App"
import {Provider} from 'react-redux'
import store from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById("root")
)
위 코드는 index.js(index.html과 같은 기본 화면에서 data 흐름을 제어해 주는 장소)에서 redux 문법을 활용하여 기본적인 react-redux 구성을 해준 모습이다.
store.js
import {createStore} from 'redux'
const ADD = "ADD"
const DELETE = "DELETE"
export const addFunc = (text) => {
return {
type : ADD,
text
}
}
export const deleteFunc = (id) => {
return {
type : DELETE,
id
}
}
const reducer = (state=[], action) => {
switch(action.type){
case ADD:
return [{text : action.text, id : action.id}, ...state]
case DELETE:
return state.filter(deletedList => {
deletedList.id !== action.id
})
default :
return state
}
}
const store = createStore(reducer)
export default store
store는 별도의 파일로 구성해주는 것이 좋고, index.js와 동일한 level directory에서 생성해준다.
먼저 reducer 함수를 구성해주고,
그 후 reducer 내부의 data를 수정해주기 위해 action의 type과 text(list의 요소를 add 시) 및 id(list의 요소를 delete 시)를 각각 return하는 별도의 trigger 함수를 구성해준다.
이후 문자열을 입력받아 추가 / 삭제 시 위에서 구성한 별도의 함수들을 호출해준다.
※ 위 함수들은 최상단에 정의해주는 것이 좋고, 이 logic은 모두 store.js(store 내부 logic)에서 생성해주는 것이 바람직하다.
connect의 첫번째 인자를 통해 state를 component와 연결 가능
- 함수를 통해 store의 state를 받아오고
- 함수 내부적으로 객체를 return하여 state에 추가하며
최종적으로 추가한 state를 components와 연결하는 구조
function getCurrentState(state, ownProps) {
console.log(state, ownProps)
}
export default connect(getCurrentState) (Home)
기본적으로 store connect 구조는 위와 같이 사용하며, 이를 정리하면
위 logic을 살펴보면
위 getCurrentState 함수에서 state, ownProps를 log 출력하면 위와 같다.
이때 state는 store로부터 전달받은 state (배열), Props는 (App.js에서 정의한) react-router로부터 전달받은 Home component의 props이다.
props 확인방법
- connect할 때의 props가 먼저 log 출력
- Home이 rendering될 때의 props는 그 이후에 출력
function Home(props) {
console.log(props)
...
}
Home 화면을 rendering하는 Home function의 인자를 통해 props를 받는다.
log 출력결과를 통해 확인할 수 있고, 이 props 전달 구조는 class에서 그대로 활용할 수 있으므로 기억하고 있도록 한다.
※ remind ) connect props > Home rendering props 순으로 flow
connect의 최종 목적은 rendering component의 props에 store state를 전달해주는 것
=mapStateToProps
function getCurrentState(state) {
//console.log(state, ownProps)
return {doList : state}
}
export default connect(getCurrentState) (Home)
위와 같이 현재 store에 저장되어있는 state를 받아 객체형태로 return 해주면 연결되어있는 component에 해당 객체를 그대로 전달해줄 수 있다.
위와 같이 connect를 통해 return한 객체가 Home rendering 시 props에 그대로 출력되었으며, 이것이 connect를 사용하는 주된 목적이라 할 수 있다.
※ 이 경우 ownProps 인자는 전달받는 props 확인 목적을 제외하고는 활용할 일이 없으므로, 인자에서 제외해도 무방하다.
Home rendering (return) 구문에 전달받은 객체를 비구조화하여 활용가능
function Home({doList})
return(
<div>
<h1>TO DO LIST</h1>
<form onSubmit={onSubmit}>
<input type="text" value = {text} onChange={onChange}/>
<button>LIST ADD</button>
</form>
<ul>{JSON.stringify(doList)}</ul>
</div>
)
위 코드처럼 Home에서 비구조화를 통해 객체를 전달받고({doList}), 이를 return하면서 화면에 rendering까지 해줄 수 있다(JSON.stringify).
실제로 state가 그대로 화면에 rendering 된 것을 확인할 수 있다(공배열 상태).
connect의 두번째 인자를 통해 dispatch를 component와 연결 가능
- 함수를 통해 component에게 dispatch를 전달하고
- dispatch를 전달받은 함수는 dispatch(action)을 통해 활용할 수 있다.
function mapDispatchToProps(dispatch){
return {dispatch}
}
export default connect(mapStateToProps, mapDispatchToProps) (Home)
위 코드에서 볼 수 있듯, dispatch를 component에게 전달할 때도 connect 함수를 이용하며 이때 두번째 인자를 통해 dispatch를 전달할 수 있다.
function Home(props) {
console.log(props)
...
}
이때 전달받은 dispatch의 log를 살펴보기 위해 props를 console.log 해본다.
위 구조처럼 dispatch도 doList와 함께 비구조화하여 그대로 활용할 수 있는 방식으로 되어 있으며, 이에 따라 Home화면은 rendering 시 dispatch도 같이 사용할 수 있다.
dispatch의 인자가 action인 것으로 보아, 말 그대로 하나의 action(store 및 data에 접근할 수 있는 외부적 접근 경로)을 전달할 수 있을 것으로 보인다.
또한 store(reducer)에서 action을 전달받고 여기서 dispatch를 전달받아 state를 수정할 수 있을 것으로 예상할 수 있다.
import addFunc from '../store'
function Home({doList, dispatch}) {
//console.log(props)
const [text, setText] = useState("")
function onChange(e) {
setText(e.target.value)
}
function onSubmit(e){
e.preventDefault()
dispatch(addFunc(text))
}
위 코드와 같이 dispatch를 비구조화하여 그대로 활용할 수 있고, submit 이벤트(여기서는 클릭에 해당가 발생하면 action을 dispatch하도록 구성할 수 있다.
이 action은 store에서 일전에 정의해준 list에 객체 추가를 해주는 action trigger이다.
export const addFunc = text => {
return {
type : ADD,
text : text
}
}
이 action trigger(=action)가 dispatch를 통해 전달되면, type을 통해 추가 logic을 실행하고 해당 text 인자를 그대로 text에게 전달해준다({text : text = text}).
※이때 전달해주는 text는 Home 내부 logic에 의해 최종적으로 문자열 입력을 받은 text 변수(문자열 저장 공간)가 된다.
※ 다만 위 코드는 pseudo code의 개념이고,
실제 dispatch는increment: () => dispatch({ type: 'INCREMENT' })
형식으로
(=dispatch 자체는 객체형식으로 action을 전달해주는 방식만 사용 가능)
사용해야 한다.
dispatch를 받은 Home이 해당 내용을 화면에 구현하는 logic을 구성한다.
dispatch를 사용하는 방법은 크게 두가지가 있다.
- dispatch 인자를 store로 부터 단순히 전달받은 경우
→ 이 경우 dispatch({type:~, id:~})와 같이 반드시 객체형태의 action만 전달해줄 수 있으며, 다른 형태의 action 전달 시 object 오류 발생.- dispatch를 전달해주는 시점에서 별도의 함수를 인자에 넣어 같이 전달하는 경우
→ 이 경우 dispatch를 전달함과 동시에 action Trigger, 즉 action을 발생시키는 함수를 같이 전달해주는 경우이다.
→ 유일하게 dispatch({})의 정해진 형식이 아닌, 다른 형태로 action을 전달할 수 있는 방식이며 반드시 최종적으로는 객체형식으로 return 값을 주어야 한다.
첫번째 방식을 통해서 dispatch 할 경우
function Home({doList, dispatch}) {
//console.log(props)
const [text, setText] = useState("")
function onChange(e) {
setText(e.target.value)
}
function onSubmit(e){
e.preventDefault()
dispatch({ text: text, id: Date.now() })
setText("")
}
위 코드처럼 반드시 전달받은 dispatch 인자를 통해 객체형식으로만 action을 전달해주어야 하고, 다른 형식으로 action을 전달하면 object 오류가 발생한다.
이럴 경우 코드의 가독성도 떨어지고, 확장성에 제한이 많아진다.
지금의 dispatch logic은 위 두번째 방식을 통해 구현할 것이다.
store.js (store)
const addFunc = text => {
return {
type : ADD,
text : text,
id : Date.now()
}
}
export const actionTrigger = {
addFunc
}
기본적으로 dispatch가 전달되는 시점에서 같이 action을 전달해주는 함수로, 반드시 return 값이 객체형태가 되어야 한다.
dispatch({object}) 형태로 전달하며, 이 {object}가 위 함수를 호출하면서 return하는 값을 활용하는 구조이다.
위 addFunc의 경우, dispatch와 함께 전달되어 리스트를 추가하는 add action을 발동시키는 함수이며, 이를 export하여 Home에서 사용한다.
Home.js (Home)
import {actionTrigger} from '../store'
function mapDispatchToProps(dispatch){
return {
addFunc : (text) => dispatch(actionTrigger.addFunc(text))
//you can only use dispatch(action) structure, when you give dispatch with action aruguments.
}
}
export default connect(mapStateToProps, mapDispatchToProps) (Home)
Home 화면에서 actionTrigger를 통해 export한 addFunc 함수를 dispatch가 전달되는 시점에서 인자와 함께 동시에 store에게 전달한다.
쉽게 말하면 add를 발생시키는 logic을 함수화하여, 코드를 간결하고 보기좋게 구성이 된 것이다.
dispatch에서 호출되는 addFunc는 return 값이 객체이기 때문에, 해당 객체인 action을 그대로 전달하며 connection()함수를 통해 전달한 dispatch는 그대로 component에 추가된다.
function Home({doList, addFunc}) {
function onSubmit(e){
e.preventDefault()
addFunc(text) //call dispatch logic by specific function
setText("")
}
return(
<div>
<h1>TO DO LIST</h1>
<form onSubmit={onSubmit}>
<input type="text" value = {text} onChange={onChange}/>
<button>LIST ADD</button>
</form>
<ul>{JSON.stringify(doList)}</ul>
</div>
)
}
이때 위에서 dispatch를 전달하면서, Home은 return 값인 addFunc를 비구조화하여 사용할 수 있다.
이 return 값은 dispatch를 통해 전달된 Home의 props에서 사용이 가능하고, addFunc를 통해 정의한 함수 logic을 그대로 호출하여 최종적으로는 action을 return 하게 된다.
※ addFunc 함수를 호출하면서 현재의 text 값을 action에 전달, store는 해당 value를 text 변수에 최종 저장한다.
최종적으로 action을 store의 reducer에게 전달하여 해당 배열 요소들이 화면에 구현되는 것을 확인할 수 있다.
rendering 하는 부분을 component화 하여 코드를 refactoring 한다.
Home.js
return(
<div>
<h1>TO DO LIST</h1>
<form onSubmit={onSubmit}>
<input type="text" value = {text} onChange={onChange}/>
<button>LIST ADD</button>
</form>
<ul>{doList.map(doListToView => <Dolistview key={doListToView.id} {...doListToView} />)}</ul>
</div>
)
doList(text, id로 이루어진 배열의 객체요소)의 id, text 변수를 Dolistview component에 전달하여, 해당 component에서 화면을 구현하는 logic을 구성한다(가독성↑).
※ doList.map하면서 key값에 unique 인자인 id값을 할당해준다.
Dolistview.js(component 생성)
import React from 'react'
function Dolistview({text}){
return(
<li>
{text}
<button>DELETE</button>
</li>
)
}
export default Dolistview
component에서 전달받은 text를 비구조화하여, 화면을 rendering 하는데 사용한다.
정상적으로 화면이 구현되었고, logic이 이상없음을 확인한다.
다른 component에서 connect를 활용하여 list 삭제 logic을 구현해본다.
Dolistview.js 라는 문자열과 버튼을 화면에 나타내는 component에서의 logic을 정리해보면
function mapDispatchToProps(dispatch, ownProps){
console.log(ownProps)
return {
//deleteFunc : (id) => dispatch(actionTrigger.deleteFunc(id))
//you can only use dispatch(action) structure, when you give dispatch with action aruguments.
}
}
export default connect(null, mapDispatchToProps) (Dolistview)
위와 같이 일단 Dolistview component와 store를 comnnect하고, dispatch 인자를 연결해준다.
이때 연결한 Dolistview component에 대한 log를 하면 아래와 같이 출력된다.
이 log 출력을 통해 두 가지를 알 수 있게 된다.
따라서 ownProps.id를 활용하면 클릭한 항목의 id값을 얻게되며, 이를 deleteFunc인자에 전달하면서 dispatch하면 삭제 logic이 구현될 것이다.
button을 누르면 함수가 호출되도록 구성
function Dolistview({text, deleteFunc}){
return(
<li>
{text}
<button onClick={deleteFunc}>DELETE</button>
</li>
)
}
일전 addFunc 함수 호출과 마찬가지로 이벤트가 발생하였을 때(버튼 클릭 시) 해당 함수가 호출되면서 action을 dispatch할 수 있도록 구성한다.
component connect 및 클릭 시 dispatch되는 logic 구현
function mapDispatchToProps(dispatch, ownProps){
//console.log(ownProps)
return {
deleteFunc : () => dispatch(actionTrigger.deleteFunc(ownProps.id))
//you can only use dispatch(action) structure, when you give dispatch with action aruguments.
}
}
export default connect(null, mapDispatchToProps) (Dolistview)
최종적으로 deleteFunc가 호출되었을 때 action인자(여기서는 action 함수의 최종적으로 return되는 객체 값)가 전달되도록 mapDispatchToProps 함수를 구성한다.
※ 이때 전달받는 인자는 없고, 다만 ownProps를 전달받아 ownProps.id를 action 함수에게 전달하는 부분만 유의(무조건 함수가 호출되면서 인자를 전달하는 건 아니며, 인자가 어느 시점에서 어떻게 전달되는지 잘 고려한다).
삭제 logic이 잘 작동하는지 확인해본다.
버튼 이벤트가 실행되면서 함수만 호출하고, 인자는 전달하지 않아도 된다.
- 전달 인자는 ownProps의 id를 전달하며, 전달하면서 action 함수 호출
function Dolistview({text, deleteFunc}){
return(
<li>
{text}
<button onClick={deleteFunc}>DELETE</button>
</li>
)
}
function mapDispatchToProps(dispatch, ownProps){
//console.log(ownProps)
return {
deleteFunc : () => dispatch(actionTrigger.deleteFunc(ownProps.id))
//you can only use dispatch(action) structure, when you give dispatch with action aruguments.
}
}
text를 전달하는 dispatch logic의 경우, 상태 text 값을 인자로 전달해야 한다.
- 해당 text 인자를 전달받고, 이를 그대로 action 함수에 전달
function onSubmit(e){
e.preventDefault()
addFunc(text) //call dispatch logic by specific function
setText("")
}
function mapDispatchToProps(dispatch){
return {
addFunc : (text) => dispatch(actionTrigger.addFunc(text))
//you can only use dispatch(action) structure, when you give dispatch with action aruguments.
}
}
return은
- 변수가 될 수 있다.
- 함수, 함수 logic 자체가 될 수 있다.
- 변수를 통해 함수의 logic을 호출할 수 있다.
★ return을 통해 함수를 호출하는 구조면, 함수가 최종적으로 return하는 형태를 유의하면서 활용하도록 한다.
react-redux 공식문서(API 관련)
https://react-redux.js.org/api/connect