본문 바로가기

블록체인 기반 핀테크 및 응용 SW개발자 양성과정 일기

[98일차] 20210729 Redux 리덕스 사용법 / reducer 파일 쪼개기 / combine

반응형

Redux 데이터 중앙관리 

 

리덕스 설치 

npm i next-redux-wrapper
npm i redux
npm i react-redux
npm i redux-devtools-extension

 

Redux  devTools 확장 프로그램 설치 

 


 

어제까지 만든 react basic layout 코드에 redux  사용하는 듯 ! ? -> context 대체

REDUX
1. store/configureStore.js 만드는 이유 

context.jsx 와 비슷
- 초기값, 상태값, createContext(초기값) 
- const Store = craeteContext(initialState) -> 결과값 : component - 초기값 저장하고 내보내는 아이 

configureStore.js 는? 
- createStore === createContext 와 비슷하다. 하지만 가지는 값은 다르다
- createStore(reducer) !== createContext(초기값)     createStore는 reducer를 담음 
- reducer 안에 상태 초기값이 있음

redux와 context와 비슷 
context는 Store 초기값을 넣고 
redux는 Store에 reducer 함수를 넣음 

 

 

 

1) 아래 packages 설치 

 

2) store > configureStore.js 생성 

Components 는 .jsx 

data와 관련된 것은.js  --->  정답은 없다 (개인 취향)

import {createWrapper} from 'next-redux-wrapper'
import {createStore} from 'redux'
import reducer from '../reducers'   //index 생략 가능


const configureStore=()=>{
    const Store = createStore(reducer)
    return Store
}

// Store 자체를 가지는게 아닌 Store를 return해 주는 함수를 담아야함 
// configureStore = Store를 가지고 있는 함수 표현식에 대한 함수 
const wrapper = createWrapper(configureStore)


export default wrapper

순서 : 

(1) export default wrapper  ----> wrapper 생성 ---> (2) 

(2) createWrapper(Store)  -----> configureStore 가 필요 ---> (3)

(3) createStore(reducer) -----> reducer 필요 ----> (4)

(4) import reducer ------> reducers 폴더 생성 > index.js 파일 생성 

 

 

 

 

3) front > reducers 폴더 생성 > index.js 파일 생성 

//context와 다른점 : 여기서 초기값을 입력해줌
const initialState = {
    user:{
        IsLogin:false,
    },
    posts:{

    },
    category:{
        
    },
}

const USER_LOGIN = "USER_LOGIN"  // 오타 났을 때 대비 
const USER_LOGOUT = "USER_LOGOUT"

const reducer = (state = initialState,action)=>{
    switch(action.type){
        case USER_LOGIN:
            console.log('로그인 신호왓다')
            return{
                ...state,
                user:{
                    ...state.user,
                    IsLogin:true
                }
            }
        case USER_LOGOUT:
            return{
                ...state,
                user:{
                    ...state.user,
                }
            }
        default:
            console.log('start')        // 맨 처음 default가 실행된다. (action 값 없음)
            return state                 // defualt 가 없으면 ? 에러가 언젠가는 난다. 
    }
}

export default reducer

 

큰 테두리는 얇은 복사로 다른 메모리 참조

안에 있는 특정 값들은 참조해서 가져오는 것 (메모리 효율성을 위해) 

 

여태 ...state, 했던 부분 얕은 복사 

이번 리덕스에서 user:{ ...state.user, IsLogin:true} 포함한 이유 : 

-> 안에 있는 내용까지 바꿀 것이라서 !

 

 

 

 

 

 

 

 

 

 

_app.jsx

Provider 와 같이 어떤 부분에서 redux를 시작할지 정해주기 - 최상위 compo = _app.jsx 

import '../index.css'
import Head from 'next/head'
import Store, { initialState } from '../store/context'
import { useReducer, useContext } from 'react'
import { reducer } from '../store/reducer'

import wrapper from '../store/configureStore'

const App = ({ Component }) => {

    const globalContext = useContext(Store) // context에 있는 값을 그대로 빼와서 담음 

    const [state, dispatch] = useReducer(reducer, globalContext)

    return (
        <>
            <Head>
                <link rel="preconnect" href="https://fonts.googleapis.com" />
                <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="true" />
                <link href="https://fonts.googleapis.com/css2?family=Black+Han+Sans&display=swap" rel="stylesheet" />
            </Head>
            <Store.Provider value={{ state, dispatch }}>
                <Component /> {/* 우리가 만든 파일(index,login..)들이 여기에 위치 */}
            </Store.Provider>
        </>
    )
}

export default wrapper.withRedux(App)   //App 아래로 사용하겠다 ~

useReducer는 react-redux 에 있음 (package) 

 

 

----------------- Login 할 때 써보기 

 

login.jsx

import FormLayout from "../components/FormLayout"
import Head from 'next/head'
import Router from 'next/router'
import useInput from '../hooks/useInput'

import {useDispatch} from 'react-redux'

const Login = () => {
    const dispatch = useDispatch()

    const userid = userInput('')  // object
    const userpw = userInput('')  // object

    const handleSubmit=(e)=>{
        e.preventDefault()

        dispatch({type:"USER_LOGIN"})

        userid.value==="asdf" && userpw.value==="asdf" ? Router.push('/') : alert('id 또는 pw가 다릅니다.')
    }

reducer - console.log 찍고 확인

const reducer = (state = initialState,action)=>{
    switch(action.type){
        case USER_LOGIN:
            console.log('로그인 신호왓다')
            return{

 

 

 

사용법

context 의 경우 

1) Store 가져오기  (상태값) 

2) useContext 가져오기 (context사용하기 위한 매서드) 

3) dispatch({type:'LOGOUT'})  사용 

 

const {dispatch} = useContext(Store)  -> _app.js에서 <Store.provider value={{state,dispatch}} />  요렇게 보내 준 것 중 필요한 것을 가져올 수 있었음. 

 

 

Redux 의 경우 

1) useDispatch 가져오기  (dispatch하면 -> reducer 이미 reducer 안에 state 가 함께 있음) 

2) dispatch에 useDispatch() 함수 넣기 

3) dispatch 사용 

 

질문 : useDispatch, useSelector 

dispatch를 쓰는 Header.jsx에서 useDipatch를 가져옴

IsLogin 상태값이 필요한 Login.jsx, Logout.jsx에서는 useSelector 매서드 가져와서 필요한 변수값 추출함 

 

reducer 의 type을 변수로 빼기 

오타 대비해서 코드의 생산성은 떨어지지만 디버깅에는 좋음

reducers> index.js 추가 

const USER_LOGIN = "USER_LOGIN"  // 오타 났을 때 대비 
const USER_LOGOUT = "USER_LOGOUT"

export const UserLoginAction =()=>{
    return{
        type:USER_LOGIN
    }
}

login.jsx

import FormLayout from "../components/FormLayout"
import Head from 'next/head'
import Router from 'next/router'
import useInput from '../hooks/useInput'
// redux 
import {useDispatch} from 'react-redux'
import {UserLoginAction} from '../reducers' //가져오기 

const Login = () => {
    const dispatch = useDispatch()

    const userid = userInput('')  // object
    const userpw = userInput('')  // object

    const handleSubmit=(e)=>{
        e.preventDefault()

        dispatch(UserLoginAction())    // 요렇게 수정 

        userid.value==="asdf" && userpw.value==="asdf" ? Router.push('/') : alert('id 또는 pw가 다릅니다.')
    }

 

 

 

Header.jsx 의 context -> Redux로 교체 

header.jsx

context 지우고 useSlector 가져오기 

import Link from 'next/link'
import NavToggle from '../NavToggle'
import Styled from 'styled-components'
import { useContext } from 'react'
import Store from '../../store/context'
//redux
import {useSelector} from 'react-redux'                // 1. useSelector 가져오기 

const Header = () => {
    // context 
    // const globalStore = useContext(Store)
    // const { IsLogin } = globalStore.state

    const IsLogin = useSelector((state)=>state.user.IsLogin)         // 2. 사용
    //  ==  const {IsLogin} = useSelector((state)=>state.user)
    console.log(IsLogin)

질문 : 터미널에 console.log가 안찍히고 브라우저에 찍힘 - > 교수님은 터미널에도 찎힘 ! 

 

이미 IsLogin 삼항연산자 있어서 고칠필요 X 나머지 코드만 간략히 만들기 

import Link from 'next/link'
import NavToggle from '../NavToggle'
import Styled from 'styled-components'
import { useContext } from 'react'
import Store from '../../store/context'
//redux
import { useSelector } from 'react-redux'

const LoginComponent = () => {
    return (
        <>
            <li><Link href="/login"><a>로그인</a></Link></li>
            <li><Link href="/join"><a>회원가입</a></Link></li>
        </>
    )
}

const LogoutComponent = () => {
    return (
        <>
            <li><Link href="/logout"><a>로그아웃</a></Link></li>
            <li><Link href="/profile"><a>회원정보</a></Link></li>
        </>
    )
}

const Header = () => {
    // context 
    // const globalStore = useContext(Store)
    // const { IsLogin } = globalStore.state

    const IsLogin = useSelector((state) => state.user.IsLogin)
    //  ==  const {IsLogin} = useSelector((state)=>state.user)
    console.log(IsLogin)

    return (
        <>
            <HeaderContainer>
                {/* 로고 & 메뉴  */}
                <h1>로고</h1>
                <Gnb>
                    <li>
                        <Link href="/">
                            <a>Home</a>
                        </Link>
                    </li>
                    <li>
                        <Link href="/posts/post">
                            <a>글쓰기</a>
                        </Link>
                    </li>
                    {IsLogin === false ? <LoginComponent/> :<LogoutComponent/>}
                </Gnb>
                <NavToggle />
            </HeaderContainer>
        </>
    )
}
css생략

 

 

 

 

login 하고 -> logout 오류남 ! 

이유 : 서로 바라보고 있는 상태가 다름 

login -> useDispatch(Redux)  

logout -> useContext 

=> Context API로 만든 IsLogin 상태값과 Redux로 만든 IsLogin 값은 다름 

 

 

 

Logout.jsx 도 같은 상태값(Redux 사용되는) 으로 변경하기 

// import Store from '../store/context'
import { useContext, useEffect } from 'react'
import Router from 'next/router'
import { useDispatch } from 'react-redux'

const logout=()=>{

    // const {dispatch} = useContext(Store)
    const dispatch = useDispatch()

    useEffect(()=>{
        dispatch({type:'logout'})

        setTimeout(()=>{
            Router.back()
        },1000)
    },[]) // 빈배열일 경우render 완료 되었을 때 딱 한번 실행되는 코드가 됨

 

reducers - index.js 에 변수 추가

const USER_LOGIN = "USER_LOGIN"  // 오타 났을 때 대비 
const USER_LOGOUT = "USER_LOGOUT"

export const UserLoginAction =()=>{
    return{
        type:USER_LOGIN
    }
}

export const UserLogoutAction =()=>{
    return{
        type:USER_LOGOUT
    }
}

logout.jsx에 import 해서 변수로 넣기

// import Store from '../store/context'
import { useContext, useEffect } from 'react'
import Router from 'next/router'
//redux
import { useDispatch } from 'react-redux'
import { UserLogoutAction} from '../reducers'

const logout=()=>{

    // const {dispatch} = useContext(Store)
    const dispatch = useDispatch()

    useEffect(()=>{
        dispatch(UserLogoutAction())

        setTimeout(()=>{
            Router.back()
        },1000)
    },[]) // 빈배열일 경우render 완료 되었을 때 딱 한번 실행되는 코드가 됨

 

 

 

대분류 메뉴들도 상태값에 넣어서 관리하게 편하게 만들기 

Accordion.jsx의 변수 menu 를 잘라서( 복사 x / 자르기 o ) -> reducers>index.js의 category에 담기

//context와 다른점 : 여기서 초기값을 입력해줌
const initialState = {
    user:{
        IsLogin:false,
    },
    posts:{

    },
    category:{
        menu:[  // server db 에서 이렇게 데이터가 왔다고 가정 
            {
                id: '1',
                category: '대분류메뉴1',
                url: '/posts/1'
            },
            {
                id: '2',
                category: '대분류메뉴2',
                url: '/posts/2'
            },
            {
                id: '3',
                category: '대분류메뉴3',
                url: '/posts/3'
            },
            {
                id: '4',
                category: '대분류메뉴4',
                url: '/posts/4'
            },
            {
                id: '5',
                category: '대분류메뉴5',
                url: '/posts/5'
            }
        ]
    },
}

상태값을 가져오는 건 - useSelector 

Accordion 에 해당 상태값 가져오기 -> 뿌려주기 (map은 기존 코드엿슴)

import Styled from 'styled-components'
import Link from 'next/link'
import { useSelector } from 'react-redux'

const Accordion = ({ visible, handleToggle }) => {
    // const handleClick=(e)=>{
    //     handleToggle()
    // }
    const {menu} = useSelector((state)=>state.category)

    const category = menu.map((v, k)=>{
        return <li key={v.id} onClick={handleToggle}>
            <Link href={v.url}>
                <a>{v.category}</a>
            </Link>
        </li>
    })

질문 : 그럼 reducers> index.js data와 관련된 곳이면 db연결해서 가져오는 곳도 index.js ?? 

-> 단점 : index.js 의 파일이 너무너무 길어짐 & 하나의 파일에 코드가 길면 관리 힘듬 

-> Redux 에서 reducer를 쪼개서 사용하라고 권장 (combine) 

user , posts, category 요렇게 따로 파일 쪼개기 

 

 

Redux reducer 파일 쪼개기

1) reducers > users.js, category.js, posts.js 생성 

2) 각 파일은 보통 reducer 처럼 만들어 주기 (기본)

아래 기본 코드 3개 파일에 복붙 

const initialState ={

}

const reducer =(state=initialState, action)=>{
    switch(action.type){
        default:
            return state
    }
}

export default reducer

3) uses.js 부터 완성 시키기 

- IsLogin:false 넣기 

- users 관련한 case 들 붙여 넣기 

- users (login, logout)관련한 변수 설정한 것들도 모두 가져오기 

const initialState = {
    IsLogin: false,
}

const USER_LOGIN = "USER_LOGIN"  // 오타 났을 때 대비 
const USER_LOGOUT = "USER_LOGOUT"

export const UserLoginAction = () => {
    return {
        type: USER_LOGIN
    }
}

export const UserLogoutAction = () => {
    return {
        type: USER_LOGOUT
    }
}

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case USER_LOGIN:
            console.log('로그인 신호왓다')
            return {
                ...state,             // state 가 달라져서 요기도 이렇게 바뀜 
                IsLogin: true

            }
        case USER_LOGOUT:
            return {
                ...state,
                IsLogin: false
            }
        default:
            return state
    }
}

export default reducer

 

 

4) reducer 합치기 (COMBINE)  

combine -> switch 를 합치려고 ! 

index.js 에서 combineReducers from 'redux' 가져오기 

import { combineReducers } from "redux"
const reducer = combineReducers({
    index: (state={}, action)=>{
        switch(action.type){
            default:
                return state
        }
    },
    //user는 user.js파일에서 미리 함수로 만들어 놓아서 import 함수만 해오면 됨 
    // user: 
})

export default reducer

user.js 가져오기 

import { combineReducers } from "redux"
import user from './user'
import { combineReducers } from "redux"
import user from './user'

//context와 다른점 : 여기서 초기값을 입력해줌
const initialState = {
    user:{
        
    },
    posts:{

    },
    category:{
        menu:[  // server db 에서 이렇게 데이터가 왔다고 가정 
            {
                id: '1',
                category: '대분류메뉴1',
                url: '/posts/1'
            },
            {
                id: '2',
                category: '대분류메뉴2',
                url: '/posts/2'
            },
            {
                id: '3',
                category: '대분류메뉴3',
                url: '/posts/3'
            },
            {
                id: '4',
                category: '대분류메뉴4',
                url: '/posts/4'
            },
            {
                id: '5',
                category: '대분류메뉴5',
                url: '/posts/5'
            }
        ]
    },
}



// const reducer = (state = initialState,action)=>{
//     switch(action.type){

//         default:
//             console.log('start')
//             return state
//     }
// }

//인자값 1개 객체만 받는다. 
const reducer = combineReducers({
    index: (state={}, action)=>{
        switch(action.type){
            default:
                return state
        }
    },
    //user는 user.js파일에서 미리 함수로 만들어 놓아서 import 함수만 해오면 됨 
    user,  // == user:user 
})

export default reducer

login.jsx 에도 UserLoginAction 가져오는 경로 변경 

import {UserLoginAction} from '../reducers/user' //가져오기

logout.jsx 도 수정

import { UserLogoutAction } from '../reducers/user'

이제 menu의 값을 가져오는데도 오류가 생김 -> 아직 index.js의 reducer에 category 가 아직 없음 ! 그리고 category.js에도 아직 menu 복붙을 안함

 

index.js의 menu 짜르기 -> category.js 의 initialState에 복붙

const initialState ={
    menu:[  // server db 에서 이렇게 데이터가 왔다고 가정 
        {
            id: '1',
            category: '대분류메뉴1',
            url: '/posts/1'
        },
        {
            id: '2',
            category: '대분류메뉴2',
            url: '/posts/2'
        },
        {
            id: '3',
            category: '대분류메뉴3',
            url: '/posts/3'
        },
        {
            id: '4',
            category: '대분류메뉴4',
            url: '/posts/4'
        },
        {
            id: '5',
            category: '대분류메뉴5',
            url: '/posts/5'
        }
    ]
}

const reducer =(state=initialState, action)=>{
    switch(action.type){
        default:
            return state
    }
}

export default reducer

index.js - category 가져와서 category reducer에 넣기

import { combineReducers } from "redux"
import user from './user'
import category from './category'

//context와 다른점 : 여기서 초기값을 입력해줌
const initialState = {
    user:{},
    posts:{},
    category:{},
}

// const reducer = (state = initialState,action)=>{
//     switch(action.type){

//         default:
//             console.log('start')
//             return state
//     }
// }

//인자값 1개 객체만 받는다. 
const reducer = combineReducers({
    index: (state={}, action)=>{
        switch(action.type){
            default:
                return state
        }
    },
    //user는 user.js파일에서 미리 함수로 만들어 놓아서 import 함수만 해오면 됨 
    user,  // == user:user 
    category,
})

export default reducer

=> 필요없는 부분 제거 

import { combineReducers } from "redux"
import user from './user'
import category from './category'

//인자값 1개 객체만 받는다. 
const reducer = combineReducers({
    index: (state={}, action)=>{
        switch(action.type){
            default:
                return state
        }
    },
    //user는 user.js파일에서 미리 함수로 만들어 놓아서 import 함수만 해오면 됨 
    user,  // == user:user 
    category,
})

export default reducer

index: state가 없다. case HYDRATE 추가해주기 (이유:오리무중)

import {HYDRATE} from 'next-redux-wrapper'

import { combineReducers } from "redux"
import user from './user'
import category from './category'

//인자값 1개 객체만 받는다. 
const reducer = combineReducers({
    index: (state={}, action)=>{
        switch(action.type){
            case HYDRATE:
                return {
                    ...state,
                    ...action.payload
                }
            default:
                return state
        }
    },
    //user는 user.js파일에서 미리 함수로 만들어 놓아서 import 함수만 해오면 됨 
    user,  // == user:user 
    category,
})

export default reducer

 

 

 


 

DevTools

 

Redux  devTools 확장 프로그램 설치하기 

 

여기서 middlewares : 부모님이다

reducer(action)   <= middlewares =>   state 

이제 reducer(action)와 state 가 직접적으로 바로 통신하는게 아니라 middlewares를 통해 통신 ~ 

middlewares 부모님 허락 맡고 만나,,

중간다리 역할이다 (내일 middleware에 대해 더 딥하게 공부 !) 

어떠한 action(type)이 들어오면 ---> middlewares ---> state

 

* middlewares의 역할 : 데이터를 가져온다거나 / 몇 초 뒤에 실행한다거나 / 비동기 통신 등등... 

 

 

 

1. configureStore.js 

추가한 코드의 의미 : state 상태값을 만들 때, middlewares를 통하도록 하겠다. 

createStore의 첫 번째 인자값 : reducer

createStore의 두 번째 인자값 : middlewares를 사용할 지 정하기 

import {createWrapper} from 'next-redux-wrapper'
import {applyMiddleware, compose, createStore} from 'redux'
import reducer from '../reducers'   //index 생략 가능
import {compostWithDevTools} from 'redux-devtools-extension'

const configureStore=()=>{
    const middlewares = []
    const enhancer = process.env.NODE_ENV === 'production'     //우리는 develope mode 
    ? compose(applyMiddleware(...middlewares))
    : composeWithDevTools(applyMiddleware(...middlewares))  // 우리가 사용할 코드 

    const Store = createStore(reducer, enhancer)
    return Store
}

// Store 자체를 가지는게 아닌 Store를 return해 주는 함수를 담아야함 
const wrapper = createWrapper(configureStore,{
    debug:process.env.NODE_ENV === 'development'
    //debug:true (개발모드이므로) 와 같은 의미 
})


export default wrapper

composeWithDevTools -> 이 아이가 dispatch를 루팡해옴 

실행할 떄마다 dispatch를 저장해 놓음 

dispatch로 모든 코드 실행의 history를 냄겨놓음 / 순서대로 수집까지함 => composeWithDevTools의 역할 

우리가 원하는 곳으로 갈 수 있음 

 

createWrapper의 두 번째 인자값 : option 

debug 추가하면 터미널에 아래와 같은 코드가 나옴 

브라우저에도 똑같이 뜬다. 

 

 

 

2) redux 확장 프로그램 확인해보기

왼쪽 : dispatch action 값 쭈루루룪 나옴 

오른쪽 action: action 

state : 현재 내 상태값 

diff : 바뀐 값 

 

로그인을 하게 되면

dispatch 사용했기 때문에 왼쪽에 USER_LOGIN (action.type)이 생김 (action 값 훔쳐옴)

login, logout etc ...dispatch 사용할 때마다 ~ 수집 

action 값도 확인할 수 있음 

 

JUMP 기능 

-> 로그인 / 로그아웃 상태로 돌아가기 

장점 : 개발 중 로그인했을 때 쓸 수 있는 상황 등 이럴 때 편함 !  => 상태관리가 편하다 

코드의 생산성이 조금 떨어지지만 나머지 모든 것들이 좋다 ! 

 

 

질문 

1. {} === {}  false 인 이유??

2. 상수 ? 

반응형