반응형

여기까지가 어제 구현 한 부분이다

modules에서 redux모듈counter.jstodos.js를 만들고 index.js에서 combinedReducers로 합쳐서 rootReducer를 만들어 준 후에 rootReducer를 이용해서 store를 만들어주고

만들어진 store를 컨테이너컴포넌트인 CounterContainers.js 에서 접근해서 상태값과 디스패치를 받아서

프레젠테이션컴포넌트인 Counter.js에게 props로 필요한 값을 전달해서

상태값dispatch함수를 화면구현에 사용했다.

 

오늘은 여기에 저번에 Redux모듈로 만들어놓고 사용하지 않은 todos를 이용하는 todolist를 만들어주겠다

 

화면을 구성하는 요소인 프레젠테이션 컴포넌트부터 만들었다.

 

components / Todos.js

import React, { useState } from 'react';
import Todolist from './Todolist';

const Todos = ({onCreate,onToggle,todos}) => {
    const [text,setText] = useState("") //input에 들어갈 상태값.
    const onChange = (e) =>setText(e.target.value) //input태그에 값이 입력되면 상태값이변함
    const onClick = () =>{
        onCreate(text);
        /*
        (text) =>({
        	type:ADD_TODO,
        	todo:{id:nextId,text: text,done:false}
    	}) 
        */
        setText(''); // 등록한후에는 input창을 초기화하기위해서 상태값 비워줌
    }
    return (
        <div>
            <div>
                <input value={text} onChange={onChange}></input>
                <button onClick={onClick}>등록</button> 
            </div>
                <Todolist onToggle={onToggle} todos={todos}/>
        </div>
    );
};

export default Todos;

인풋과 등록창을 만들어주고 등록되었을때 화면에 뜨는 부분은 Todolist컴포넌트로 따로 만들었다.

input 태그에도 상태값을 하나 추가로 주기위해 따로 useState를 이용해서 상태값을 만들어줬다.

 

이제 할일을 추가했을 때 띄워줄 Todolist컴포넌트를 만들어준다.

 

components / Todolist.js

import React from 'react';

const Todolist = ({todos,onToggle}) => {
    return (
        <div>
            <ul>
                {todos.map(todo => <li key={todo.id} 
                onClick={()=>onToggle(todo.id)} 
                style={{textDecoration: todo.done ? 'line-through':'none'}}>
                    {todo.text}
                </li>)}
            </ul>
        </div>
    );
};

export default Todolist;

Todos컴포넌트에서 onClick해주면 상태값의 todos에 저장된다.

case ADD_TODO:
   return[
      ...state, //원래있던값들을 저장하고
      action.todo // todo:{id:nextId,text: text , done:false} 새로운 todo추가 
    ]

todosTodolist.js에 넘겨주고 배열메서드인 map으로 <li></li> 안에 값을 넣어서 만들어준다. 

li를 클릭하면 isDone이 토글되고

isDone이 false일 때는 유지하고 true가 되면 밑줄을 그어주도록 스타일도 적용해 준다.

style={{textDecoration: todo.done ? 'line-through':'none'}}

 

isDone을 토글 시켜줄 onToggle TodosContainer에서 만들어서 전달된다.

onToggle아이디를 전달받아서

case TOGGLE_TODO:
     return state.map(todo => todo.id === action.id ? {...todo, done:!todo.done} : todo )
default:

todos를 다시 맵으로 돌려서 해당하는 id와 같은 아이디가 있으면 todo.done을 토글 시켜준다.

 

 

 

어제 했던 redux를 이어서 한 건데 코드를 한번 더 읽어보고 다시 써보니까 이제 동작원리가 조금은 이해가 되는 것 같다.

여기저기서 불러쓰는 게 많아서 복잡해보였는데 불러서 쓰는 요소들을 밑에 주석으로 써가면서 보니까 읽기에도 편하고 뭐하는 함수였는지 바로바로 알수있었다. 주석은 좀 더 간결하게 쓰는게 좋을 것 같지만 일단은 내가 이해를 해야 하니 길어도 어쩔 수 없다..ㅋㅋㅋ

반응형
반응형

오늘은 react에서 상태를 관리해 주는 기술react-redux를 배웠다.

 

Redux

 

Redux 시작하기 | Redux

소개 > 시작하기: Redux를 배우고 사용하기 위한 자료

ko.redux.js.org

Redux(리덕스)는 JavaScript(자바스트립트) 상태관리 라이브러리이다.

(react에서뿐만 아니라 바닐라 자바스크립트에서도 사용가능)

 

redux의 규칙 3가지

1. 하나의 애플리케이션 안에는 하나의 스토어가 있다.

2. 상태는 읽기 전용이다.

3. 리듀서는 순수한 함수여야 한다.

 

redux사용방법

//스토어 생성하기
const sotre = Redux.createStore(리듀서함수)
redux작동 그래프

액션 객체를 dispatch로 보내주면 dispatch는 스토어에 있는 reducer함수를 불러서

함수에 액션객체를 매개변수로 새로운 상태값을 반환해 준다.

 

1. 액션객채(Action)

상태를 업데이트할 때 액션을 전달해 준다.

액션은 객체로 표현되며 type속성을 가지고 있다.

{
	type:"INCREMENT"
}

2. 액션 생성함수(Action creator)

액션객체를  퉤! 하고 뱉어주는 함수

function increment(add){
	return{
		type:"INCREMENT",
		add: add
	}
}

useReducer에서도 액션을 사용했지만 액션을 함수로 만들어서 사용한 적은 없었는데.

이렇게 사용하면 동일한 액션객체를 전달해줄 수 있고 매개변수를 받아서 액션객체에 추가해 줄 수도 있다.

 

3. 리듀서(reducer)

상태를 업데이트시켜주는 함수이다.

리듀서는 두 가지 파라미터를 받아온다.( 상태값, 액션객체 )

function reducer(state,action){
	return newState;
}

리듀서에서는 새로운 상태값을 리턴해준다.

 

4. 스토어(store)

리덕스에서는 한  애플리케이션당 하나의 스토어를 만든다.

스토어 안에는 현재의 상태와 리듀서 내장함수들이 들어가 있다.

dispatch(), subscribe(), getState()

 

5. 구독(subscribe)

스토어의 내장함수 중 하나이다. 함수 형태의 값을 파라미터로 받아오며 subscribe함수에

특정 함수를 전달하면 액션이 디스패치 될 때마다 전달해 준 함수가 호출됨.

//자바스크립트에서는 상태가 변해도 리렌더링 되지 않기 때문에 상태값이 변할 때 함수를 다시 호출해서 바뀐 값을 다시 넣어서 실행해줘야 한다.

 

 

여기서 redux를 가져올 주소를 복사해서 redux를 사용할 파일에 연결해 준다.

<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.2.0/redux.min.js"></script>

이제 redux의 기능들을 해당 애플리케이션 파일에서 사용할 수 있다.

redux의 필요성을 알아보기 위해 같은 기능을

redux를 사용하지 않고 구현한 코드와 사용해서 구현한 코드 두 개를 쓰겠다.

 

withoutredux.html (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">
    <title>Document</title>
    
    <style>
        .component{
            border: 5px solid #000;
            padding:10px;
        }
    </style>
</head>
<body>
    <div id="red"></div>
    <div id="blue"></div>
    <script>
        function red(){
            document.querySelector("#red").innerHTML = `
            <div class="component">
                <h1>red</h1>
                <button onclick="
                document.querySelector('#red').style.backgroundColor='red'
                document.querySelector('#blue').style.backgroundColor='red' ">버튼</button>    
            </div>
            `
        }    
        function blue(){
            document.querySelector("#blue").innerHTML = `
            <div class="component">
                <h1>blue</h1>
                <button onclick="
                document.querySelector('#blue').style.backgroundColor='blue'
                document.querySelector('#red').style.backgroundColor='blue'">버튼</button>    
            </div>
            `
        }  

        red();
        blue();  

        </script>
</body>
</html>

<div> 태그를 두 개 만들어주고 아래에서 자바스크립트로 div태그 안에 들어갈 요소들을 만들어주고

버튼에는 이벤트를 만들어 주었다. 

red 쪽에 있는 버튼을 클릭하면 div두 개의 배경색이 빨간색으로 변하고 

blue 쪽에 있는 버튼을 클릭하면 div 두 개의 배경색이 파란색으로 변한다.

버튼을 클릭하면 배경색이 변한다.

 이 코드에서 div를 추가해서 yellow를 하나 더 만들어 준다고 가정하면

        function yellow(){
            document.querySelector("#yellow").innerHTML = `
            <div class="component">
                <h1>blue</h1>
                <button onclick="document.querySelector('#blue').style.backgroundColor='yellow'
                document.querySelector('#red').style.backgroundColor='yellow'
                 document.querySelector('#yellow').style.backgroundColor='yellow'
                ">버튼</button>    
            </div>
            `
        }

이런 함수를 하나 추가해줘야 하고 위의 redblue 함수도 yellow박스에 색이 들어갈 수 있도록

yellow div에 배경색을 주는 코드를 한 줄 추가해줘야 한다 이렇게 하나의 태그가 늘어날 때 다른 함수들도 다 변경되어야 하는 관련성이 있다.

 

redux를 사용하면 이런 관련성을 낮춰줄 수 있다.

 

withredux.html (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">
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.2.0/redux.min.js"></script>
    <style>
        .component{
            border: 5px solid #000;
            padding:10px;
        }
    </style>
</head>
<body>
    <div id="red"></div>
    <div id="blue"></div>
    <script>
        //1.리듀서 함수 만들기
        function reducer(state,action){
            //state초기값을 위해 처음에 호출
            if(state === undefined){
                return {color:"white"}
            }
            let newState;
            if(action.type === "CHANGE_COLOR"){
                newState={...state , color:action.color}
                console.log(newState)
            }
            return newState;
        }
        // 2. redux스토어 생성하기.
        const store = Redux.createStore(reducer);
        
        function red(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#red").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>red</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'red'})">
                버튼</button>    
            </div>
            `
        }    
        function blue(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#blue").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>blue</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'blue'})">
                버튼</button>    
            </div>
            `
        }    

        //구독하기
        store.subscribe(red); //상태가업데이트될때 해당함수를 다시 호출
        store.subscribe(blue);
        red();
        blue()
    </script>
    
</body>
</html>

이렇게 써주면 아까 리덕스를 쓰지 않고 만들었던 코드와 똑같은 기능을 할 수 있게 만들어졌다.

여기서는 새로운 div를 추가해 줘도 다른 함수를 수정해주지 않아도 된다.

 

yellow div 추가

<!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">
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.2.0/redux.min.js"></script>
    <style>
        .component{
            border: 5px solid #000;
            padding:10px;
        }
    </style>
</head>
<body>
    <div id="red"></div>
    <div id="blue"></div>
    <div id="yellow"></div>
    <script>
        //1.리듀서 함수 만들기
        function reducer(state,action){
            //state초기값을 위해 처음에 호출
            if(state === undefined){
                return {color:"white"}
            }
            let newState;
            if(action.type === "CHANGE_COLOR"){
                newState={...state , color:action.color}
                //  newState = Object.assign({},state,{color:action.color}) // object.assign 객체 복사 ({복사될대상},복사할객체,추가할내용)
                console.log(newState)
            }
            return newState;
        }
        // 2. redux스토어 생성하기.
        const store = Redux.createStore(reducer);
        
        function red(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#red").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>red</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'red'})">
                버튼</button>    
            </div>
            `
        }    
        function blue(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#blue").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>blue</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'blue'})">
                버튼</button>    
            </div>
            `
        }    
        function yellow(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#yellow").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>yellow</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'yellow'})">
                버튼</button>    
            </div>
            `
        }    



        //구독하기
        store.subscribe(red); //상태가업데이트될때 해당함수를 다시 호출
        store.subscribe(blue);
        store.subscribe(yellow);
        red();
        blue();
        yellow();
    </script>
    
</body>
</html>

 

 

 

이제 리액트에서 redux를 사용해 보자

 

React에서 redux사용하기

 

redux, react-redux 설치하기

npm install redux
npm install react-redux

 

redux사용법 

스토어 생성하기

//createStore 불러오기
import { legacy_createStore as createStore } from 'redux';
//store 만들기
const store = createStore(리듀서함수)

 

스토어 사용하기

스토어를 사용하려면 상위컴포넌트에서 Provider로 감싸줘야 하위 컴포넌트에서 store를 불러서 쓸 수 있다.

import { Provider } from 'react-redux';

<Provider store={store}>
   <App />
</Provider>

 

* 리덕스 스토어 상태 조회하는 Hook함수 useSelector

import { useSelector } from 'react-redux";

const state = useSelector(state=>state)

* 리덕스 스토어 디스패치 반환하는 Hook함수 useDispatch

import { useDispatch } from 'react-redux";

const dispatch = useDispatch()

​이렇게 어디서든 디스패치와 상태값을 불러서 사용할 수 있다.

 

예제 만들기

리덕스 모듈 생성 후 스토어 생성하기

리덕스 모듈이란 액션 타입, 액션생성함수, 리듀서 가 모두 들어있는 자바스크립트 파일.

여러 개의 리덕스 모듈을 생성하여 하나의 리덕스 모듈로 합쳐서 사용한다.

* combineReducers({ 리덕스모듈 객체 }) 함수를 사용하여 리덕스 모듈을 합쳐줌.

import { combineReducers } from "redux";
const rootReducer = combineReducers({
    리덕스모듈1: 리덕스모듈1,
    리덕스모듈2: 리덕스모듈2
}) ---> 리듀서를 합쳐줌

counter 모듈 + todos 모듈-----> 루트리듀서 ----> 스토어 생성.

 

주의할 점

액션 타입 - 다른 리덕스 모듈과 겹치지 않게 하기 위해 컴포넌트 이름의 접두사를 앞에 붙여줘야 한다

counter/SET_DIFF

액션 생성함수 -액션객체를 리턴해주는 함수. (export로 내보내기)

리듀서 - 컴포넌트 이름으로 만들어지며 state 파라미터에 디폴트 초기값을 지정합니다.

(export default로 내보내기)

state=initialState

 

모듈들을 관리해 줄 modules폴더를 만들고 그 안에 모듈들을 만들어준다.

 

modules/counter.js

//액션 : 타입 만들기
const SET_DIFF =  "counter/SET_DIFF";
const INCREMENT = "counter/INCREMENT";
const DECREMENT = "counter/DECREMENT";

//액션 생성 함수 만들기
export const setDIFF = (diff)=>({type:SET_DIFF,diff});
export const increase = () =>({type:INCREMENT});
export const decrease = () =>({type:DECREMENT});
//초기 상태 선언
const initialState = {
    number: 0 ,
    diff: 1
}
//리듀서 선언
export default function counter(state=initialState , action){
    switch(action.type){
        case SET_DIFF:
            return{
                ...state,
                diff:action.diff
            }
        case INCREMENT:
            return{
                ...state,
                number:state.number + state.diff
            }
        case DECREMENT:
            return{
                ...state,
                number:state.number - state.diff
            }
        default:
            return state;
    }
}

 

modules/todos.js

//액션타입선언
const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_TODO = 'todos/TOGGLE_TODO';

//액션 생성 함수
let nextId = 1
export const addTodo = (text) =>({
    type:ADD_TODO,
    todo:{id:nextId,text: text,done:false}
})
export const toggleTodo = (id)=>({
    type:TOGGLE_TODO,
    id:id
})

//초기상태값
//초기 상태는 배열이어도 되고,원시타입(숫자,불린,문자열) 객체도 가능하다.
const initialState = [
    // {
    //     id:1,
    //     text:"해야할일",
    //     done:false
    // }
]
//리듀서

export default function todos(state=initialState,action){
    switch(action.type){
        case ADD_TODO:
            return[
                ...state,
                action.todo
            ]
        case TOGGLE_TODO:
            return state.map(todo => todo.id === action.id ? {...todo, done:!todo.done} : todo )
        default:
            return state
    }
}

 

​modules/index.js  rootreducer 만들기

import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";

//한프로젝트에 리듀서가 여러개일때 하나로 합쳐서 사용 => rootreducer 루트리듀서
//combinereducers( )

const rootReducer = combineReducers(
    {
        counter:counter,
        todos:todos
    }
    )
export default rootReducer

 

스토어 생성하기

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import { devToolsEnhancer } from '@redux-devtools/extension'; // 크롬확장 redux devtools
//스토어 만들기
const store = createStore(rootReducer,devToolsEnhancer());

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

store를 만들고 Provider로 묶어줘야 하위 컴포넌트에서 store요소를 불러서 사용할 수 있다.

이제 이 값들을 불러서 화면을 구성하고 상태값을 변경해 줄 컴포넌트를 만들어주면 된다.

 

컴포넌트 만들기


1. 프레젠테이셔널 컴포넌트 

실제 보이는 태그들을 만들어주는 컴포넌트, redux스토어에 직접 접근하지 않음
필요한 값은 props로 받아와서 사용하는 컴포넌트, 주로 UI를 선언하는 것에 집중

 

2. 컨테이너 컴포넌트

 

프레젠테이셔널 컴포넌트를 감싸고 store에 접근해서 값을 props로 프레젠테이셔널로 전달.
리덕스 스토어의 상태를 조회하거나, 액션을 디스패치 할 수 있는 컴포넌트.
프리젠테이셔널 컴포넌트를 불러와서 사용

 

꼭 이렇게 만들어줄 필요는 없지만 기능적으로 분리해 주면 나중에 에러가 났을 때 찾기 편하다

 

1. 프레젠테이셔널 컴포넌트

components/Counter.js

import React from 'react';

const Counter = ({number,diff,onIncrease,onDecrease,onsetDIFF}) => {
    const onChange = (e)=>{
        onsetDIFF(Number(e.target.value)) //(diff)=>({type:SET_DIFF,diff});
    }
    return (
        <div>
            <h1>{number}</h1>
            <div>
                <input type="number" value={diff} min="1" onChange={onChange}/>
                <button onClick={onIncrease}>+</button>
                <button onClick={onDecrease}>-</button>

            </div>
        </div>
    );
};

export default Counter;

 화면을 구성해 주는 요소들이 들어있고 필요한 값들은 props로 컨테이너 컴포넌트한테서 받아온다.

 

2. 컨테이너 컴포넌트

containers/CounterContainer.js

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Counter from '../components/Counter';
import { decrease, increase,setDIFF } from '../modules/counter';

const CounterContainer = () => {
    //useSelector() 는 리덕스스토어의 상태를 조회하는 hook이다

    const { number,diff } = useSelector(state=>state.counter);
    const dispatch = useDispatch();
    // 각 액션들을 디스패치 하는 함수
    const onIncrease = ()=> dispatch(increase());
    const onDecrease = ()=>dispatch(decrease());
    const onsetDIFF = (diff) => dispatch(setDIFF(diff));
    return (
        <Counter number={number} diff={diff} onIncrease={onIncrease} onDecrease={onDecrease} onsetDIFF={onsetDIFF}/>
    );
};

export default CounterContainer;

 

컨테이너 컴포넌트에서 Counter컴포넌트를 불러서 사용하고 Props로

함수=dispatch(액션생성함수)와 상태값을 보내주고 있다.

 

App.js에서 CounterContainer 컴포넌트를 불러서 사용하면 끝

 

redux자체는 그렇게 되게 어렵지는 않은데 이번에 redux를 배우면서 새로운 개념들을 같이 배워서 이해하기가 정말 너무 힘들었다. 모듈을 다 나눠서 만들고 합쳐서 하나로 만들고 액션객체도 일일이 만들어줬었는데 이번에는 액션객체를 만드는 함수를 만들어서 사용하니 갑자기 연결된 애들이 많아져서 뭐가 어디서 불러서 쓰고 어디에 들어간게 맞는건지 구분을 못해서 수업시간에 좀 힘들었다.. 그래도 redux는 많이 사용하고 있다고하니 연습해서 꼭 잘 쓰고싶다.. 포기하지말자.. 수료 2달남았다.. ㅠㅠ

반응형

+ Recent posts