책 읽다가 코딩하다 죽을래

리액트 심화반 - 3주차 개발일지(immer, redux-action, Debounce, Throttle, useCallback) 본문

GD프로젝트/개발일지

리액트 심화반 - 3주차 개발일지(immer, redux-action, Debounce, Throttle, useCallback)

ABlue 2021. 7. 29. 14:52

 

 

 

 

GD프로젝트 리액트 심화반

3주 차가 시작되었다.

이번에도 새롭게 배운 내용과 결과물들을 설명해보겠다.

 

 

 

목차

1. 새롭게 배운 내용(immer, redux-action, Debounce, Throttle, useCallback)

2. 3주 차 과제

3. 배운 내용 프로젝트에 적용

 

 

 


1. 새롭게 배운 내용


 

 

 

1 -1 immer, redux-actions

더보기

사실 2주 차에 배웠던 것들인데

그때는 배운 내용들이 정리가 안되어서

3주 차에 쓰고 있다..

 

 

3주 차에는 redux를 더욱 쉽고 이용하기 간편한 모듈들에 대해 배웠다.

 

redux-logger : 웹상에서 redux의 action이 일어날 때마다 콘솔에서 action 내역을 이쁘게 보여주는 모듈

 

 

 

 

이런 식으로 action이 일어날 때마다 action 이전 state 값, action , action 이후 state 값 

한 프로젝트 안에 action을 많이 다룬다면 logger 사용을 강력 추천한다. 

 

 

history@4.10.1 , connected-react-router@6.8.0 : redux에서 history 객체를 사용할 수 있게 해주는 모듈

immer : reducer 불변성 관리하게 해주는 모듈

redux-action : redux action 구문을 더욱 편리하게 작성할 수 있게 해주는 모듈

 

 

 

모듈 설치 방법

yarn add redux react-redux redux-thunk redux-logger history@4.10.1 connected-react-router@6.8.0 immer redux-actions

npm i redux react-redux redux-thunk redux-logger history@4.10.1 connected-react-router@6.8.0 
immer redux-actions

 

초기 셋팅 방법

차례 : 리덕스 스토어 만들기 -> 리덕스 주입하기 -> 스토어 및 라우터 적용하기 -> action 및 router 적용하기

 

워낙 편리하게 해주는 모듈이다 보니 초기 세팅 난이도가 거의 이클립스 설치 및 환경변수 급의 난이도를 자랑한다.

 

 

프로젝트 폴더 구조
ㄴ public
ㄴ node_modules
ㄴ src
     ㄴ index.js
     ㄴ App.js
     ㄴ redux
           ㄴ configureStore.js
           ㄴ module
                  ㄴ user.js

 

 

1. 리덕스 스토어 만들기 (configureStore.js)

 

 

import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import { createBrowserHistory } from "history";
import { connectRouter } from "connected-react-router";

import User from "./modules/user";		// user Action Module
import Post from "./modules/post";		// post Action Module
import Image from "./modules/image"		// image Action Module
export const history = createBrowserHistory();

// rootReducer 만들기
const rootReducer = combineReducers({
    user: User,
    router: connectRouter(history),
});

//미들웨어 준비
const middlewares = [thunk.withExtraArgument({history:history})];        // 사용하고 싶은 미들웨어를 이 배열안에 넣어줘야한다.  //withExtraArgument에다 history를 넣어주면 reducer로 가기전에 history를 사용할 수 있다.

// 지금이 어느 환경인 지 알려준다. (개발환경, 프로덕션(배포)환경 ...) 내가 어느 환경에 있는지 알려주는 것이다.
const env = process.env.NODE_ENV;

// redux-logger를 사용하기 위한 작업
if (env === "development") {
  const { logger } = require("redux-logger");       // 왜 굳이 import 가 아닌 require를 썻는가? 이 모듈은 dev환경에서만 쓰이고 build(배포)환경에선 쓰이지 않기 때문이다. 굳이 import해서 프로그램 용량을 차지하면 낭비이다.
  middlewares.push(logger);
}
// redux devTools 설정
const composeEnhancers =
  typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__         // 지금 돌아가는 환경이 브라우져가 아니면 window라는 객체가 없다. 브라우져일때만 실행되게 한후
    ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({                                 // 리덕스 데브툴즈를 실행한다.
        // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
    })
: compose;
// 미들웨어 묶기
const enhancer = composeEnhancers(          // 우리에게 사용하는 미들웨어를 묶어준다.
    applyMiddleware(...middlewares)
);
// 스토어 만들기
let store = (initialStore) => createStore(rootReducer, enhancer);

export default store();

 

 

한 번에 이해하기 어려울 것이다.

사실 지금 나도 이 코드를 안보고 쓰라하면 절대 못쓴다...

코드를 외울려 하지 말고 코드 한줄한줄이 무엇을 의미하는지를 보자

 

 

2. 리덕스 주입하기 (index.js)

 

 

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import './index.css';
import App from './shared/App'

import store from './redux/configureStore'
ReactDOM.render(
  <Provider store={store}>			// 스토어 주입하기!!
    <App />
  </Provider>,
  document.getElementById('root')
);

 

 

3. 스토어 및 라우터 적용하기(App.js)

 

 

import './App.css';
import React from "react";

import {Route} from "react-router-dom";
import {ConnectedRouter} from 'connected-react-router'
import {history} from '../redux/configureStore'

import PostList from "../pages/PostList";
import Login from "../pages/Login";
import Signup from "../pages/Signup";


function App() {
  return (
    <React.Fragment>
        <ConnectedRouter history={history}>			// connectedRouter의 history 주입하기!!
          <Route path="/" exact component={PostList} />
          <Route path="/login" exact component={Login} />
          <Route path="/signup" exact component={Signup}/>
        </ConnectedRouter>
    </React.Fragment>
  );
}

export default App;

 

 

4. action 및 reducer 만들기(user.js)

 

 

import { createAction, handleActions } from "redux-actions";
import { produce } from "immer";

// actions
const LOG_IN = "LOG_IN";
const LOG_OUT = "LOG_OUT";

// action creators
const logIn = createAction(LOG_IN, (user) => ({ user }));		// 여기에 들어간 인자는 action.payload에 들어가게 된다.
const logOut = createAction(LOG_OUT, (user) => ({ user }));

// initialState
const initialState = {
	user: null,
    is_login: false,
}

// reducer
export default handleActions({ 
	[LOG_IN] : (state, action) => produce(state, (draft) => { 		// 액션 명을 기술한다.
		draft.user = action.payload.user;		// draft는 state을 접근할 수 있는 객체(?)이며, 아까 createAction 넣었던 인자를 action.payload로 접근할 수 있다.
		draft.is_login = true;
	}), 
	[LOG_OUT] : (state, action) => produce(state, (draft) => { 
		draft.user = null;
		draft.is_login = false;
	}), 
}, initialState );		// reducer 대신 handleActions을 쓰고 1번째 인자는 reducer내용을 작성하고 2번째 인자는 초기 state 값을 넣는다.

// action creator export
const actionCreators = {
	logIn,
    logOut,
};

export { actionCreators };

 

redux-action은 이런 식으로 만든다.

 

그럼 redux-action과

본래의 redux와 차이점을 비교해보자 

 

 

// actions
const LOG_IN = "LOG_IN";
const LOG_OUT = "LOG_OUT";

// action creators
const logIn = (user) => {
	return { type: LOG_IN, user }
}
const logOut = (user) => {
	return { type: LOG_OUT, user }
}
// initialState
const initialState = {
	user: null,
    is_login: false,
}

// reducer
export default reducer(state = initialState, action = {}){ 
	switch(action.type){
    	case "LOG_IN" : {
        	return {...state, user: action.user};
        }
        case "LOG_OUT" : {
        	return {...state, user: null, is_login : false};
        }
    default: return state;
	}
}


// action creator export
const actionCreators = {
	logIn,
    logOut,
};

export { actionCreators };

 

 

 

 

다른 것들은 거의 흡사하고 action creator와 reducer이 두 가지가 다르다.

 

다만 immer를 쓰는 이유는

객체는 const로 선언해도 내용이 수정될 수 있다. 그래서 스프레드 문법(...state) 등을 이용해서 수정되지 않게 사용하는 것이 redux의 국룰입니다.

그런데 객체 구조가 복잡해지면 코드를 짜기가 번거로워진다. 그래서 불변성을 신경 쓰지 않는 것처럼 써도 알아서 불변성을 유지해주는 것이 immer이다.

 

이러한 점을 고려하여 원래 redux와 redux-action을 잘 선택해서 사용하길 바란다.

 

 

 

 

1 -2 Debounce와 throttle

더보기
const onChangeEvent = (e) => {
	setInputValue(e.target.value);
}

return(
	<input onChange={onChangeEvent}></input>
)

 

 

input onChange이벤트는 input의 current.value가 달라질 때마다 일어나는 이벤트이다.

우리는 위의 코드를 이용해서 input에 들어있는 value를 state에 저장할 수 있다.

 

네이버의 검색엔진 또한 onChange 이벤트를 이용해 검색바 밑에 연관검색어를 추천해준다.

그런데 value가 달라질 때마다 onChange 이벤트를 발생하게 되면 자원낭비와 부자연스러운 연출이 일어난다.

 

우리는 위에 사진처럼 감사합니다 를 입력했을 때 정확히는 감사합니다의 연관검색어를 원하는데

value값이 달라질때마다 이벤트를 일어나게 한다면

 

ㄱ -> 가 -> 감 -> 감ㅅ -> 감사 -> ... -> 감사합니다

에 대한 무수히 많은 이벤트가 일어나게 되고 연관검색어 화면도 무수히 많은 변화가 생기게 되어

눈에도 보기 좋지 않다. 

즉 사용자가 검색어를 모두 입력할 때까지 발생한 이전 이벤트는 필요 없다.

그래서 사용되는 것이 Debounce와 Throttle이다

 

 

Debounce는 이벤트가 일어나면 일정 시간 기다렸다가 이벤트를 수행하게 한다.

기다리고 있는 시간에 같은 이벤트가 또 들어오면 이전 이벤트를 취소된다.

 

Throttle은 일정 시간 동안 일어난 이벤트를 모아서 주기적으로 1번씩 실행해준다

 

이 두 가지 기능은 자바스크립트 유틸리티 라이브러리에 들어있다

 

 

npm i lodash
yarn add lodash

 

import React from 'react'

const Search = () => {

	const onChange = (e) => {
    	console.log(e.target.value);
    }
    
    return (
        <div>
                <input type="text" onChange={onChange}/>
        </div>
    )
}

export default Search

 

여기 아주 간단한 코드의 결과를 보자

input창의 감사합니다를 입력했을 때이다.

 

 

 

 보시다시피 이벤트가 일어날 때마다 모든 변화 내역을 바로 콘솔에 찍어준다.

 

먼저 Debounce부터 해보자

 

import React from 'react'
import _ from "lodash"		// lodash 모듈 불러오기
const Search = () => {

    const debounce = _.debounce((e) => {
    	console.log("debounce ::: ", e.target.value);
    }, 1000);		// 첫번째 인자는 실행해야할 함수 두번째 인자는 주기를 말한다. 단위는 ms
    // 이 코드를 해석하자면 onChange 이벤트가 일어난 후 1초후에 함수가 실행되는 것이며
    // 1초가 지나기 전 onChange 이벤트가 발생하면 이전 이벤트는 실행되지 않고 취소된다.
   

    return (
        <div>
                <input type="text" onChange={debounce}/>
        </div>
    )
}

export default Search

 

 

 

 

 이런 식으로 실행된다.

 

그다음 Throttle 차례이다.

 

 

import React from 'react'
import _ from "lodash"

const Search = () => {

    const throttle = _.throttle((e) => {
    	console.log("throttle ::: ", e.target.value);
    }, 1000);
	// throttle의 입력인자는 Debounce와 똑같다

        <div>
                <input type="text" onChange={throttle}/>
        </div>
    )
}

export default Search

 

 

 

이런 식으로 주기적으로 발생한다.

 

검색 엔진이나 onChange이벤트를 세세하게 다루려 할 때 사용하는 것을 추천한다.

 

 

 

 

1 -3 useCallback

더보기

이 항목은 바로 위인 Debounce와 throttle를 먼저 보신 후에 보는 것을 추천합니다.

 

 

useCallBack은 메모이제이션과 관련이 깊은 함수이다.

 

 

import React, { useState } from 'react'
import _ from "lodash"
const Search = () => {

    const debounce = _.debounce((e) => {console.log("debounce ::: ", e.target.value);}, 1000);;
    const [text, setText] = useState('');

    const onChange = (e) => {
        setText(e.target.value);
        debounce(e);
    })
    
    return (
        <div>
                <input type="text" onChange={onChange}/>
        </div>
    )
}

export default Search

 

 

이런 식으로 onChange 이벤트가 일어나 state가 변경되어 컴포넌트가 일어나는 상황이라고 해보자

 

 

 

이러면 Debounce가 소용없게 된다.

왜냐하면 onChange이벤트가 일어날 때마다 리랜더링이 일어나고 Debounce 함수마저 초기화되어버리기 때문이다.

쉽게 말해

 

Debounce : 어? 이벤트가 또 일어났네? 이전 이벤트는 무시해야지~

 

(리랜더링으로 인한 함수 초기화)

 

Debounce : 헤헤헤 이전 이벤트가 뭐였더라?? 엇 또 이벤트 들어왔다 푸하하하하

 

 

이러한 상황이 반복되는 것이다. 즉 onChange 이벤트가 일어날 때마다 Debounce가 늘어나고

이런 Debounce들이 독립적으로 실행된 다음 사라지고 있는 것이다.

 

이렇게 컴포넌트가 리랜더링 일어났다고 해서 이벤트도 다시 초기화되는 것을 막으려면

useCallback을 사용해야 한다.

 

 

import React, { useState, useCallback } from 'react'
import _ from "lodash"
const Search = () => {

    const debounce = _.debounce((e) => {console.log("debounce ::: ", e.target.value);}, 1000);
    const [text, setText] = useState('');
    const keyPress = useCallback(debounce, []);     // 첫번째인자는 useCallback이 관리할 함수이고, 두번째인자가 변하면 함수도 변한다는 뜻이다.
    const onChange = (e) => {
        setText(e.target.value);
        keyPress(e);
    }
    return (
        <div>
                <input type="text" onChange={onChange}/>
        </div>
    )
}

export default Search

 

 

이러면 컴포넌트가 리랜더링 될 때마다 함수가 초기화되지 않는다.

두 번째 인자는 그 안에 들어간 변수가 값이 변하면 함수를 초기화하겠다는 것이다.

 

 

 


2. 3주 차 과제


더보기

 

3주 차 과제

구현된 기능 : 게시물 업로드, 수정

사용된 것 : redux-action, redux-thunk, immer, redux-logger, history@4.10.1 , connected-react-router@6.8.0, sessionStorage

 

firebase Storage와 Authentication도 다뤄보았는데 점점 나만의 인스타그램이 되는 것 같아 좋다

 

 

업로드 된 사진은 Firebase Storage에 저장된다.

 

회원가입한 유저는 Firebase Auth에 저장되고 관리된다.

 

사용자가 로그인하면 세션에 유저정보가 저장된다.

 

유저 정보는 귀중한 것이니 로컬 스토리지에 저장하면 안 된다!!

 

 

 

 

 

 


3. 배운 내용 프로젝트에 적용


더보기

  

redux-action과 immer를 적용해보았다.

아직 길지는 않지만 이렇게 사용해보니

 

 

 

 이젠 props가 아닌 redux-action을 사용해서 데이터 송수신이 자유도가 높아지고 편하다.

또한 sessionStorage를 사용하니 새로고침을 해도 컴포넌트끼리 엮어주는 중요한 인덱스 데이터가 날아가지 않아 사용자 입장에선 더욱 편해질 것 같다.

 

앞으로 redux-action, immer 등 은 자주 사용될 것 같다.