[Redux] redux 중급 - react 기반의 redux connect 활용하기

Hyo Kyun Lee·2021년 9월 1일
0

Redux

목록 보기
6/9

1. Redux와 React의 활용방안

React는 Virtual DOM, 기본적으로 DOM(html)에서 변화한 부분만 rendering 한다.
※ react는 변경사항을 rendering 한다.

Redux는 data를 store에 저장하고, 이를 reducer를 통해 일괄적으로 관리하고 수정하며 subscribe를 통해 구현한다.
※ redux의 store.subscribe는 reducer가 호출되면서 자동으로 같이 동작한다.
※ redux 상태변화 후 subscribe가 동작하여 변화한 상태변화를 rendering하는 구조(일종의 hook)

Redux와 React를 활용하게 되면

  • 일단 두 개념들이 rendering 할 구조적으로 비슷한 점이 있다.
  • rendering하는 data의 흐름이 산포되어 있는 것을 일방향으로 전환하여 사용할 수 있다.
  • 그만큼 data의 유지, 관리, 보수 등이 유용해진다.
  • data rendering 시점을 redux의 trigger 함수를 통해 일괄적으로 적용할 수 있다(subscibe를 통해 data 상태변화 시점을(=reducer 호출시점) 인식하고, 이를 rendering 하는 방식으로 구성)

2. react-redux 기본 구성(index.js/store.js)

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 구성을 해준 모습이다.

  • Provider라는 특수 tag에서 store 속성(default 값)에 store(사용자 정의)를 할당한다.
  • Provider가 fragment가 되어, 내부에서 App화면이 구현된다.

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)에서 생성해주는 것이 바람직하다.

3-1. store의 상태(state)를 component와 연결하기(connect) - 개념 / 구조

connect의 첫번째 인자를 통해 state를 component와 연결 가능

  • 함수를 통해 store의 state를 받아오고
  • 함수 내부적으로 객체를 return하여 state에 추가하며

최종적으로 추가한 state를 components와 연결하는 구조

function getCurrentState(state, ownProps) {
    console.log(state, ownProps)
}

export default connect(getCurrentState) (Home)

기본적으로 store connect 구조는 위와 같이 사용하며, 이를 정리하면

  • component를 export하는 시점에서 connect API를 사용한다.
  • connect는 인자를 통해 함수/객체와 연결할 수 있으며, 내부 함수/객체 logic으로 store state를 받아오고 이를 component에게 전달할 수 있다(※ connect의 내장 함수가 component props와 연결되면서 component와 연결 가능한 구조).
  • connect와 연결된 component는 그 다음 인자를 통해 전달한다(Home).

위 logic을 살펴보면

  • connect와 Home component를 연결하였고, 이 component의 props가 내장 함수로 전달
    (공식 문서의 mapStateToProps가 connect 내장 함수로 전달되는 logic을 뜻함)
  • connect 내장 함수를 통해 store의 state와 component의 props를 전달

위 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

3-2. store의 상태(state)를 component와 연결하기(connect) - connect를 통해 component에 객체 추가하기

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 된 것을 확인할 수 있다(공배열 상태).

3-3. dispatch(action)를 component와 연결하기(connect) - 개념 / 구조

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를 수정할 수 있을 것으로 예상할 수 있다.

3-4. store와 component에게 action을 dispatch 하기

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을 전달해주는 방식만 사용 가능)
사용해야 한다.

3-5. dispatch logic 구성 하기

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은 위 두번째 방식을 통해 구현할 것이다.

  • dispatch를 전달하면서 action trigger함수(함수 자체가 action 객체를 return 하는 logic)를 인자로 받고, 이 함수는 action 객체를 return 한다.
  • dispatch를 호출하는 함수를 return하도록 구성하고, 이를 Home에서 비구조화를 통해 활용하는 방식으로 구성한다.

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 변수에 최종 저장한다.

3-6. 화면을 통해 구현하기

최종적으로 action을 store의 reducer에게 전달하여 해당 배열 요소들이 화면에 구현되는 것을 확인할 수 있다.

3-7. (참조) refactoring

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이 이상없음을 확인한다.

4-1. 버튼을 클릭하여 list 삭제 logic 구현하기(connect 활용하기)

다른 component에서 connect를 활용하여 list 삭제 logic을 구현해본다.

Dolistview.js 라는 문자열과 버튼을 화면에 나타내는 component에서의 logic을 정리해보면

  • 버튼을 클릭하였을 때 문자열이 삭제된다.
  • addFunc과 마찬가지로, 버튼이 눌렀을 때의 이벤트가 발동되면 deleteFunc가 dispatch 될 것이다.
  • dispatch는 id값을 전달받는다(store에서 정의해준 그대로).
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 출력을 통해 두 가지를 알 수 있게 된다.

  • dispatch 인자는(connect를 통한 dispatch logic) 기본적으로 화면이 rendering 될 때, rendering 완료 후 재호출된다.
  • rendering 완료 이후의 시점에서, 자신이 전달받은 component의 props(ownProps)를 받아온다(이때 props는 react-router를 통해 전달받은 id,text 값이 유일하다).

따라서 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 함수에게 전달하는 부분만 유의(무조건 함수가 호출되면서 인자를 전달하는 건 아니며, 인자가 어느 시점에서 어떻게 전달되는지 잘 고려한다).

4-2. 화면 구현 확인

삭제 logic이 잘 작동하는지 확인해본다.

5. (참조) 인자전달

버튼 이벤트가 실행되면서 함수만 호출하고, 인자는 전달하지 않아도 된다.

  • 전달 인자는 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.
    }
}

6. (참조) return

return은

  • 변수가 될 수 있다.
  • 함수, 함수 logic 자체가 될 수 있다.
  • 변수를 통해 함수의 logic을 호출할 수 있다.

★ return을 통해 함수를 호출하는 구조면, 함수가 최종적으로 return하는 형태를 유의하면서 활용하도록 한다.

7. 참조링크

react-redux 공식문서(API 관련)
https://react-redux.js.org/api/connect

0개의 댓글