'분류 전체보기'에 해당되는 글 84건

  1. 2019.09.05 기본 개념 및 간단한 웹서버 구현
  2. 2019.09.03 webpack - resolve
  3. 2019.09.03 react-router
  4. 2019.09.02 middleware
  5. 2019.09.01 Todo-list (5) : redux 적용
  6. 2019.08.29 Counter (2) : 멀티 카운터
  7. 2019.08.29 Counter (1) : 카운터 만들기
  8. 2019.08.29 Redux Life Cycle
  9. 2019.08.29 Todo-list (4) : 리렌더링 최적화 하기
2019. 9. 5. 10:56

1. 기본 개념

* Node.js

: V8-engine based runtime which allows using Javascript in server

V8 자바스크립트 엔진을 기반으로 하는 서버에서 자바스크립트를 사용하게 해주는 런타임

 

- 특징

1. single thread

2. non-blocking I/O

3. event-drive

 

* Global Object

1. console : 콘솔 창에 결과를 보여주는 객체

 - dir(object) : 객체의 속성들을 출력

 - time(id) : 실행시간 측정을 위한 시작시간 기록

 - timeEnd(id) :  실행시간 측정을 위한 종료시간 기록

 

2. process : 프로세스의 실행에 대한 정보를 다루는 객체

 - argv : 프로세스를 실행할 때 전달되는 파라미터 정보를 담고 있는 배열 객체

 - env : 환경변수 정보를 담고 있는 객체

 - exit() : 프로세스를 끝내는 메소드

 

3. exports : 모듈을 다루는 객체

 

* Global variable

- __dirname : 폴더의 전체 패스가 출력됨

- __filename : 파일의 전체 패스가 출력됨

 

* Modules

: 별도의 파일로 분리된 독립 기능의 모음

 

1. 외장 모듈 : 다른 사람이 만들어 둔 모듈

 

2. 내장 모듈

- os : 시스템 정보를 알려줌

- path : 파일 패스를 다루는 메소드 제공

  - join() : 여러개의 이름을 합쳐 하나의 파일 패스로 만들어 줌

  - dirname() : 파일 패스에서 디렉터리 이름을 반환

  - basename() : 파일 패스에서 파일의 확장자를 제외한 이름 반환

  - extname() : 파일 패스에서 파일의 확장자 반환

- url : 주소 문자열 안에 있던 정보를 나누고 객체를 만들어 객체의 속성으로 보관

- querystring : 요청 파라미터(query string)를 쉽게 분리

- fs : 파일시스템에 접근및 파일을 다루는 메소드 제공

-http : 웹 서버 기능을 담당하는 서버객체를 만들 수 있음

-events : 이벤트 핸들링 메소드 제공 

-util : 상속메소드 제공

 

* Handling url and request parameters

  - url.parse() : 주소문자열을 파싱하여 url객체를 생성

  - url.format() : url객체를 주소문자열로 변환

  - querystring.parse() : 주소문자열을 파싱하여 url객체를 생성

  - querystring.format() : url객체를 주소문자열로 변환

 

* Handling events

- require('events').EventEmitter.on(event, listener) : 지정한 이벤트 리스너를 추가

- require('events').EventEmitter.once(event, listener) : 지정한 이벤트 리스너를 추가하지만 한 번 실행후 자동으로 리스너 제거

- require('events').EventEmitter.removeListener(event, listener) : 지정한 이벤트에 대한 리스너 제거

- require('events').EventEmitter.emit(event) : 지정한 이벤트 발생

 

* request vs response

- request : 클라이언트가 서버에게 웹페이지를 보여달라고 말하는 것

- reponse : 서버가 클라이언트에게 요청받은 것에 대한 대답으로, 웹페이지 내용을 표현하기 위해 html문서로 주는것

 

* HTTP 패킷

 클라이언트가 서버로 요청을 했을때, 보내는 데이터

-  HTTP패킷의 구조는 크게 헤더  바디로 나뉘어진다.

- 헤더에는 7가지 HTTP 메서드 방식(GET, POST, PUT, DELETE, PATCH, ...)중 무엇을 썻는지, 클라이언트의 정보, 브라우저 정보, 접속할 URL 등등 과 같은 클라이언트 정보를 담는다. 

- 바디는 보통 비어있다. 하지만, 특정 데이터를 담아서 서버에게 요청을 보낼 수 있다.

 

* GET방식 vs POST방식 

두 방식 모두, 서버에 요청을 하는 메서드이다.

 

- get 방식 : 'HTTP패킷 헤더'부분에 요청 정보를 넣어 보낸다

- post 방식 : 'HTTP패킷 바디'부분에 요청 정보를 넣어 보낸다

 

get 방식은 주소 표시줄에 입력한 내용이 나타나며 256byte~4096byte까지의 데이터만을 서버로 전송할 수 있다. 주소 표시줄에 입력한 내용이 노출되기 때문에 보안상의 문제가 민감한 경우에는 사용하지 않는다.
- post 방식은 입력된 내용의 크기에 제한을 받지 않고 입력한 내용이 노출되지 않기 때문에 회원가입, 로그인 시 등에 많이 사용

 

 

2. 간단한 웹서버 구현

* 서버 구현

//src/index.js
const http = require('http');

//웹 서버 객체 생성
const server = http.createServer();

//웹 서버를 시작하며 3000번 포트에서 대기하도록 설정
const port = 3000;
server.listen(port,()=>{
    console.log('웹서버가 시작됨 [port : ' + port + ']');
});

//클라이언트 연결 이벤트(connection) 처리 : 클라이언트가 웹서버에 연결되면 connection 이번트 발생
server.on('connection', (socket)=>{

    //클라이언트의 ip와 port 정보 확인
    const addr = socket.address();
    console.log('클라이언트 접속 : %s, %d', addr.address, addr.port);
});

//클라이언트 요청 이벤트(request) 처리 : 클라이언트가 특정패스로 요청을 하면 request 이벤트 발생
server.on('request', (req,res)=>{
    console.log('request 이벤트 발생');
    console.dir(req);
    
    // 클라이언트로 응답을 보냄
    res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'}); //응답으로 보낼 헤더
    res.write("<!doctype html>");
    res.write("<html>");
    res.write("<head>");
    res.write("<title>nodeTest</title>");
    res.write("</head>");
    res.write("<body>");
    res.write("<p>hello node</p>");
    res.write("</body>");   
    res.write("</html>");
    res.end();     //전송
});

//서버 종료 이벤트 처리
server.on('close', ()=>{
    console.log('서버가 종료 됨');
});

MIME Type (Multipurpose Internet Mail Extensions :내용이 어떤 형식인지 알려주기 위한 인터넷 표준) -> content-type의 값

- text/plain : 일반 텍스트 문서

- text/html

- text/css

- text/xml

- image/jpeg, image/png

- vedio/mpeg, audio/mp3

- application/zip

 

* 서버 실행

node src
Posted by yongminLEE
2019. 9. 3. 13:39

Webpack에서 Resolve 설정을 추가해주면 React 프로젝트의 루트디렉토리를 설정합니다.

// webpack.config.js
// webpack.dev.config.js
var path = require('path');
/* .. 코드 생략 .. */
resolve: {
    root: path.resolve('./src')
}

이렇게하면 코드에서 import 시 path에서 간단하게 접근할 수 있게 해줍니다.

 

resolve 셋팅 전

import Home from './components/Home';
// or
import Home from './../components/Home';

 

resolve 셋팅 후

import Home from 'components/Home';

'Javascript > React' 카테고리의 다른 글

#3 React Event Handling  (0) 2020.02.12
#2 React Component  (0) 2020.02.12
#1 basic react  (0) 2020.02.12
#0 React intro  (0) 2020.02.12
react-router  (0) 2019.09.03
Posted by yongminLEE
2019. 9. 3. 13:37

1. previous page VS SPA

- previous page

: 유저가 요청할 때마다 페이지를 새로고침하며, 페이지를 로딩할 때마다 서버에서 리소를 전달받아 해석한 후 렌더링. HTML파일 또는 템플릿 엔진(ex: pug)등을 사용해서 뷰를 어떻게 보일지도 서버에서 담당

=> 단점 : 서버에서 렌더링을 담당하므로 그만큼 서버자원 소모->불필요한 트래픽낭비

=> 해결 : 리액트와 같은 라이브러리나 프레임워크를 사용하여 뷰 렌더링을 유저의 웹브라우저가 담당하도록하고, app을 우선 웹 브라우저에 로드시킨 후 필요한 데이터만 전달

 

- SPA

: 서버에서 제공하는 페이지가 하나이지만, 웹브라우저에서 나머지 페이지들을 정의. 다른 페이지로 이동할 때는 서버에 새로운 페이지를 요청하는 것이 아니라, 새 페이지에서 필요한 데이터만 받아 와 그에 따라 웹브라우저가 다른 종류의 뷰를 만든다.

 

- routing

: 주소에 따라 다른 뷰를 보여주는 것

 

- react-router

: 서드 파티 라이브러리로, 페이지 주소를 변경했을 때 주소에 따라 다른 컴포넌트를 렌더링하고, 주소정보를 컴포넌트의 props로 전달해서 컴포넌트 단에서 주소 상태에 따라 다른 작업을 하도록 설정가능

 

2. react-router 라이브러리를 이용한 SPA 개발

2-1. 프로젝트 생성및 컴포넌트 준비

$ create-react-app react-router-project
$ yarn add creat-router-dom

 

2-2. 컴포넌트 준비

//src/App.js
import React from 'react';

function App() {
  return (
    <div className="App">
    	react-router
    </div>
  );
}

export default App;

 

//src/Root.js
import React from 'react';
import App from './App';

//BrowserRouter : HTML$의 history api를 사용하여 새로고침하지 않고도 페이지 주소를 교체
import {BrowserRouter} from 'react-router-dom'; 

const Root = () => {
    return (
        <BrowserRouter> 
            <App />
        </BrowserRouter>
    );
};

export default Root;

 

//src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Root from './Root';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<Root />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

 

2-3. 기본 라우트 설정

//src/pages/Home.js
import React from 'react';

const Home = () => {
    return (
        <div>
            <h1>HOME</h1>
        </div>
    );
};

export default Home;

 

//src/pages/About.js
import React from 'react';

const About = () => {
    return (
        <div>
            <p>this is about page</p>
        </div>
    );
};

export default About;

 

export {default as Home} from './Home';
export {default as About} from './About';

 

//src/App.js
import React from 'react';

// router
import {Route} from 'react-router-dom';
import {Home, About} from './pages';

function App() {
  return (
    <div className="App">
      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
    </div>
  );
}

export default App;

 

2-4. 라우트 파라미터와 쿼리 읽기

//src/App.js
import React from 'react';

// router
import {Route} from 'react-router-dom';
import {Home, About} from './pages';

function App() {
  return (
    <div className="App">
      <Route exact path="/" component={Home} />
      <Route path="/about/:name?" component={About} /> 
    </div>
  );
}

export default App;

- params를 지정할 때는 :key형식으로 설정

- :name값을 선택적으로 입력받을 수 있게 뒷부분에 ?를 입력

//src/pages/About.js

import React from 'react';
import queryString from 'query-string';

const About = ({location, match}) => {
    const query = queryString.parse(location.search);
    const {color} = query //const color = query.color;

    return (
        <div>
            <h1 style={{color}}>About</h1>
            <p>this is about page</p>
            <p>params : {match.params.name}</p>
        </div>
    );
};

export default About;

- query-string 라이브러리는 문자열로 된 쿼리를 객체 형태로 파싱

 

2-5. 라우터이동 : Link컴포넌트, NavLink 컴포넌트, 자바스크립트에서 라우팅

//src/components/Menu.js
import React from 'react';
import {Link} from 'react-router-dom';
import {NavLink} from 'react-router-dom';

const Menu = () => {
    const activeStyle = {
        color:'green',
        fontSize:'2rem'
    }

    return (
        <div>
            <ul>
                <li><Link to="/">HOME</Link></li>
                <li><Link to="/about">ABOUT</Link></li>
                <li><Link to="/about/react">ABOUT REACT</Link></li>
                <li><a href="/about">ABOUT with 새로고칭</a></li>
                <li><a href="/about/react">ABOUT REACT with 새로고칭</a></li>
                <li><NavLink exact to="/" activeStyle={activeStyle}>HOME</NavLink></li>
                <li><NavLink exact to="/about" activeStyle={activeStyle}>ABOUT</NavLink></li>
                <li><NavLink exact to="/about/react" activeStyle={activeStyle}>ABOUT REACT</NavLink></li>
            </ul>
        </div>
    )
};

export default Menu;

- <a href>...</a> : 페이지를 새로고침하면서 로딩

- Link 컴포넌트 : 페이지를 새로고침하여 불러오지 않고, 주소 창 상태를 변경하고 원하는 라우트로 화면을 전환

- NavLink 컴포넌트 : 현재주소와 해당 컴포넌트의 목적지 주소가 일치하면 특정스타일 또는 클래스 지정 가능

 

//src/pages/Home.js
import React from 'react';

const Home = ({history}) => {
    return (
        <div>
            <h1>HOME</h1>
            <button onClick={()=>{history.push('./about/JS')}}>JS로 라우팅</button>
        </div>
    );
};

export default Home;

 

2-6. 서브라우터 : 라우트 안의 라우트 

//src/components/Menu.js
import React from 'react';
import {NavLink} from 'react-router-dom';

const Menu = () => {
    const activeStyle = {
        color:'green',
        fontSize:'2rem'
    }

    return (
        <div>
            <ul>
                <li><NavLink exact to="/" activeStyle={activeStyle}>HOME</NavLink></li>
                <li><NavLink exact to="/about" activeStyle={activeStyle}>ABOUT</NavLink></li>
                <li><NavLink exact to="/about/react" activeStyle={activeStyle}>ABOUT REACT</NavLink></li>
                <li><NavLink exact to="/postList" activeStyle={activeStyle}>POST LIST</NavLink></li>
            </ul>
        </div>
    )
};

export default Menu;

 

//src/App.js
import React from 'react';
import Menu from './components/Menu';

// router
import {Route} from 'react-router-dom';
import {Home, About, PostList} from './pages';

function App() {
  return (
    <div className="App">
      <Menu />
      <Route exact path="/" component={Home} />
      <Route path="/about/:name?" component={About} />
      <Route path="/postList" component={PostList} />
    </div>
  );
}

export default App;

 

//src/pages/PostList.js
import React from 'react';
import { Post } from 'pages';
import { Link, Route } from 'react-router-dom';

const PostList = ({ match }) => {
    return (
        <div>
            <h2>POST LIST</h2>
            <ul>
                <li><Link to={`${match.url}/1`}>POST #1</Link></li>
                <li><Link to={`${match.url}/2`}>POST #2</Link></li>
                <li><Link to={`${match.url}/3`}>POST #3</Link></li>
            </ul>
            <Route exact path={match.url} render={() => (<p>choose a post</p>)} />
            <Route exact path={`${match.url}/:id`} component={Post} />
        </div>
    );
};

export default PostList;

 

//src/pages/Post.js
import React from 'react';

const Post = ({match}) => {

    return (
        <div>
            <p>포스트 id : {match.params.id}</p>
        </div>
    );
};

export default Post;

 

//src/pages/index.js
export {default as Home} from './Home';
export {default as About} from './About';
export {default as Post} from './Post';
export {default as PostList} from './PostList';

 

2-7. 라우트로 사용된 컴포넌트가 전달받는 props : location, match, history

 

- location.pathname 은 현재 브라우저상의 위치를 알려줍니다. 이 값은 어떤 라우트에서 렌더링하던 동일합니다.

- match 관련은 설정한 Route 와 직접적으로 관계된 값만 보여줍니다.

  • Posts 를 보여주는 라우트에선 :id 값을 설정하지 않았으니 path 와 url 이 둘다 /posts 입니다.
  • Post 를 보여주는 라우트에선 path 의 경우엔 라우트에서 설정한 path 값이 그대로 나타납니다. url 의 경우엔 :id 부분에 값이 들어간 상태로 나타납니다.- histroy는 현재 라우터를 조작할 때 사용

'Javascript > React' 카테고리의 다른 글

#3 React Event Handling  (0) 2020.02.12
#2 React Component  (0) 2020.02.12
#1 basic react  (0) 2020.02.12
#0 React intro  (0) 2020.02.12
webpack - resolve  (0) 2019.09.03
Posted by yongminLEE
2019. 9. 2. 12:19

1. middleware?

: 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기 전에 사전에 지정된 작업들을 실행

-> 액션과 리듀서 사이의 중간자

 

1-1. loggerMiddleware.js 구현

const middleware = store => next => action => {
    // 현재 스토어 상태 값 기록
    console.log('현재 상태', store.getState());
    // 액션 기록
    console.log('액션',action);

    // 액션을 다음 미들웨어 또는 리듀서에 전달
    const result = next(action);

    // 액션 처리 후 스토어의 상태를 기록
    console.log('다음 상태', store.getState());
    
    return result; // 여기에서 반환하는 값은 store.dispatch(ACTION_TYPE)했을 때 결과로 설정한다.

}

export default middleware;// 내보내기

- next(action) 을 했을 때는 그다음 처리해야 할 미들웨어로 액션을 넘겨주고, 추가로 처리할 미들웨어가 없다면 바로 리듀서에 넘겨준다

- store.dispatch는 다음 미들웨어로 넘기는 것이 아니라 액션을 처음부터 디스패치한다

 

1-2. 미들웨어 적용

import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import middleware from './lib/middleware';

// 미들웨어가 여러 개일 경우 파라미터로 전달하면 된다. (applyMiddleware(a,b,c))
// 미들웨어 순서는 여기에서 전달한 파라미터 순서대로 지정한다.
const store = createStore(modules, applyMiddleware(middleware));

export default store;

 

1-3. redux-logger 라이브러리

import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import {createLogger} from 'redux-logger';

const logger = createLogger();
const store = createStore(modules, applyMiddleware(logger));

export default store;

 

1-4 . 결과

*크롬브라우저의 콘솔창 확인

 

 

2. 미들웨어로 비동기 액션 처리

: 여러 오픈소스 라이브러리 존재 : redux-thunk, redux-saga, redux-pender, redux-promise-middleware etc...

 

2-1. redux-thunk 모듈

- thunk : 특정작업을 나중에 할 수 있도록 함수 형태로 감싼 것

- redux-thunk : 함수를 디스패치함으로써 일반 액션 객체로는 할 수 없는 작업을 가능하게함

 

ex) 1초 뒤에 액션이 디스패치되는 코드

import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import {createLogger} from 'redux-logger';
import ReduxThunk from 'redux-thunk';

const logger = createLogger();
const store = createStore(modules, applyMiddleware(logger, ReduxThunk));

export default store;

 

import {handleActions, createAction} from 'redux-actions';

//action types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

//action creators
export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);

//thunks
export const incrementAsync = () => dispatch => {
    // 1초 뒤 액션 디스패치
    setTimeout(
        () => { dispatch(increment()) }, //함수를 dispatch
        1000
    );
}
export const decrementAsync = () => dispatch => {
    // 1초 뒤 액션 디스패치
    setTimeout(
        () => { dispatch(decrement()) }, //함수를 dispatch
        1000
    );
}

//reducer
export default handleActions({
    [INCREMENT]: (state, action) => state +1,
    [DECREMENT]: (state, action) => state -1
}, 0);

 

3. Promise

- Promise : 비동기 처리를 다루는데 사용하는 객체

function printLater(number, fn) {
    setTImeout(
        function() {
            console.log(number);
            if(fn) fn();
        },
        1000
    );
}

//콜백지옥
printLater(1, function() {
    printLater(2, function() {
        printLater(3, function() {
            printLater(4);
        })
    })
});

위와 같은 콜백지옥( 비동기적 작업에 의한 깊고 복잡해진 코드)을 Promise를 통해 다음과 같이 해결

function printLater(number) {
    return new Promise( // Promise 생성 후 리턴
        (resolve, reject) => {
            if ( number > 4 ) {
                return reject('number is greater than 4'); // reject 는 오류를 발생시킨다.
            }
            setTimeout( // 1초 뒤 실행
                () => {
                    console.log(number);
                    resolve(number + 1); // 현재 숫자에 1을 더한 값을 반환한다.
                }, 1000
            );
        }
    );
}

printLater(1)
    .then( num => printLater(num) )
    .then( num => printLater(num) )
    .then( num => printLater(num) )
    .then( num => printLater(num) )
    .catch( e => console.log(e) );

Promise 에서 결과 값을 반환할 때는 resolve(결과 값) 을 작성하고, 오류를 발생시킬 때는 reject(오류)를 작성.

- 여기에서 반환하는 결과 값은.then(), 오류 값은.catch() 에 전달하는 함수의 파라미터로 설정됨.

 

4. redux-thunk와 axios를 사용한 웹 요청 처리

- axios : Promise 기반 HTTP 클라이언트 라이브러리

yarn add axios

 

 

4-1. post 모듈 : API함수, 액션, 액션생성함수, thunk, reducer

// src/modules/post.js
import {handleActions, createAction} from 'reudx-actions';

import axios from 'axios';

//API function
funciton getPostAPI(postId) {
    return axios.get(`url/${postId}`);	//Promise 객체형태로 반환
}

//action types
const GET_POST_PENDING = 'GET_POST_PENDING';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_FAILURE = 'GET_POST_FAILURE';

//action creators
const getPostPending = createAction(GET_POST_PENDING);
const getPostSuccess = createAction(GET_POST_SUCCESS);
const getPostFailure = createAction(GET_POST_FAILURE);

//thunk
export const getPost = (postId) => dispatch => {
    dispatch(getPostPending()); // 요청 시작했다는 것을 알림
    
    // 요청 시작. 여기에서 만든 promise를 return해야 나중에 컴포넌트에서 호출할 때 getPost().then(...)을 할 수 있다.
    return getPostAPI(postId)
    .then( () => {
        // 요청이 성공했다면 서버 응답 내용을 payload로 설정하여 GET_POST_SUCCESS 액션을 디스패치한다.
        dispatch(getPostSuccess(response));
        
        // 후에 getPostAPI.then 을 했을 때 then에 전달하는 함수에서 response 에 접근할 수 있게 한다.
        return response;
    } )
    .catch( error => {
        // 오류가 발생하면 오류 내용을 payload로 설정하여 GET_POST_FAILURE 액션을 디스패치한다.
        dispatch(getPostFailure(error));
        
        // error를 throw하여 이 함수를 실행한 후 다시 한 번 catch를 할 수 있게 한다.
        throw(error);
    } );
}

const initialState = {
    pending: false,
    error: false,
    data: {
        title: '',
        body: ''
    }
}

//reducer
export default handleAction({
    [GET_POST_PENDING]: (state, action) => {
        return {
            ...state,
            pending: true,
            error: false
        };
    },
    [GET_POST_SUCCESS]: (state, action) => {
        const {title, body} = action.payload.data;
        return {
            ...state,
            pending: false,
            data: {
                title,
                body
            }
        };
    },
    [GET_POST_FAILURE]: (state, action) => {
        return {
            ...state,
            pending: false,
            error: true
        }
    }
}, initialState);

- thunk : 요청상태에 따라 액션을 디스패치

 

4-2. post 모듈의 리듀서를 루트 리듀서에 넣어준다

import {combineReducers} from 'redux';
import counter from './counter';
import post from './post';

export default combineReducers({
    counter,
    post
});

 

4-3. counter의 기본 값을 postId로 사용하므로 1로 설정, 0이면 오류 발생

// scr/modules/counter.js
(...)
export default handleActions({
    [INCREMENT]: (state, action) => state + 1,
    [DECREMENT]: (state, action) => state - 1
},1); // counter의 기본 값을 postId로 사용하므로 1로 설정, 0이면 오류 발생

 

4-4. App 컴포넌트에서 액션으로 웹 요청 시도

// src/App.js
import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as counterActions from './modules/counter';
import * as postActions from './modules/post';

class App extends Component {
    loadData = () => {
        const {PostActions, number} = this.props;
        PostActions.getPost(number);
    }

    componentDidMount() {
        this.loadData();
    }
    
    componentDidUpdate(prevProps, prevState) {
        // 이전 number 와 현재 number 가 다르면 요청을 시작
        if(this.props.number != prevProps.number) {
            this.loadData();
        }
    }

    render() {
        const {CounterActions, number, post, error, loading} = this.props;
        return (
            <div>
                <h1>{number}</h1>
                {
                    ( () => {
                        if (loading) return (<h2>로딩중...</h2>);
                        if (error) return (<h2>에러발생...</h2>);
                        return (
                            <div>
                                <h2>{post.title}</h2>
                                <p>{post.body}</p>
                            </div>
                        );
                    } )
                }
                <button onClick={CounterActions.increment}>+</button>
                <button onClick={CounterActions.decrement}>-</button>
            </div>
        );
    }
}

export default connect(
    (state) => ({
        number: state.counter,
        post: state.post.data,
        loading: state.post.pending,
        error: state.post.error
    }),
    (dispatch) => ({
        CounterActions: bindActionCreators(counterActions, dispatch),
        PostActions: bindActionCreators(postActions, dispatch)
    })
)(App);

 

4-5. 요청 완료후 작업및 오류 발생시 작업 추가

// es6
// loadData = () => {
//     const { PostActions, number } = this.props;
//    //this.props.Postactions.getPost(this.props.number);
//
//     PostActions.getPost(number).then(	// 요청 완료 후 작업
//         (response) => {
//             console.log(response);
//         }
//     ).catch(					// 오류 발생시 작업
//         (error) => {
//             console.log(error);
//         }
//     );
// };

//es7
loadData = async () => {
    const { PostActions, number } = this.props;
    // this.props.Postactions.getPost(this.props.number);
    
    try {
        const response = await PostActions.getPost(number); // 요청 완료 후 작업
        console.log(response);
    }
    catch (error) {                                        // 오류 발생시 작업
        console.log(error);
    }
};

- await를 쓸 함수의 앞부분에 async 키워드를 붙인다.

- 기다려야 할 Promise 앞에 await 키워드를 붙인다.

- await를 사용할 때는 꼭 try~catch문으로 오류를 처리해야 한다.

 

4-6. 요청취소기능 추가

 

 

*redux-pender, redux-promise-middleware는 잘 쓰이지 않으므로 스킵..

'Javascript > Redux' 카테고리의 다른 글

#13 Redux in React  (0) 2020.02.25
#12 Redux  (0) 2020.02.25
Redux Life Cycle  (0) 2019.08.29
Posted by yongminLEE
2019. 9. 1. 22:20

0. 디렉터리 구성

 

1. 패키지 설치

yarn add redux react-redux redux-actions immutable

 

2-1. input 모듈 생성

//dukcks structure

import {Map} from 'immutable';
import {handleActions, createAction} from 'redux-actions';

//action types
const SET_INPUT = 'input/SET_INPUT';

//action creator
export const setInput = createAction(SET_INPUT);
// ex: call -> setInput({value:'abc'}) 
// return value -> action object = {type:SET_INPUT, payload:{value:'abc'}}

//initialState
const initialState = Map({
    value:' '
});

//reducer
export default handleActions ({
    [SET_INPUT]: (state, action) => {
        return state.set('value', action.payload)
    }
}, initialState);

 

2-2. todos 모듈 생성

//dukcks structure

import {Map, List} from 'immutable';
import {handleActions, createAction} from 'redux-actions';

//action types
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

//action creators
export const insert = createAction(INSERT);
// ex: call -> insert({id:4, text:'abc', done:'false'}) 
// return value -> action object = {type:INSERT, payload:{id:4, text:'abc', done:'false'}}

export const toggle = createAction(TOGGLE);
// ex: call -> toggle(1) 
// return value -> action object = {type:toggle, payload:1}

export const remove = createAction(REMOVE);
// ex: call -> remove(2) 
// return value -> action object = {type:remove, payload:2}

//initialState
const initialState = List([
    Map({id:0, text:'todo1', done:true}),
    Map({id:1, text:'todo22', done:false}),
    Map({id:2, text:'todo333', done:false})
]);

//reducer
export default handleActions({
    [INSERT] : (state,action) => {
        console.dir(action);
        return state.push(Map({            
            id:action.payload.id,
            text:action.payload.text,
            done:action.payload.done
        }));
    },
    [TOGGLE]: (state,action) => {
        console.dir(action);
       // const {payload:id} = action;
        //List의 내장함수 get
       // console.dir(action);
        const index = state.findIndex(todo => todo.get('id') === action.payload);
        //List의 내장함수 update
        return state.update(index, item=>item.set('done',!item.get('done')));
    },
    [REMOVE]:(state,action) => {
        const {payload:id} = action;
        const index = state.findIndex(todo => todo.get('id') === id);
        return state.delete(index);
    }
}, initialState);

 

2-3. 모듈 index 생성

import input from './input';
import todos from './todos';
import {combineReducers} from 'redux';

export default combineReducers({
    input,
    todos
});

//  state { 
//     input : Map({ value:' ' }), 
//     todos :[ Map({id:0,text:" ",done: }), Map({...}), ...]
// }

 

3. 스토어 생성

import React from 'react';
import ReactDOM from 'react-dom';
import './styles/main.scss';
import App from './components/App';
import * as serviceWorker from './serviceWorker';

//creating store
import modules from './modules';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
const store = createStore(modules, window.devToolsExtension && window.devToolsExtension());

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider >
    , document.getElementById('root')
);

serviceWorker.unregister();

 

4-1. TodoInputContainer 생성

import React, {Component} from 'react';
import TodoInput from '../components/TodoInput';

import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import * as inputActions from '../modules/input';
import * as todosActions from '../modules/todos';

const mapStateToProps = (state) => ({
    value:state.input.get('value')
});

const mapDispatchToProps = (dispatch) => ({
    InputActions : bindActionCreators(inputActions, dispatch),
    TodosActions : bindActionCreators(todosActions, dispatch)
});

class TodoInputContainer extends Component {
    id=1

    getId = () => {
        return ++this.id;
    };
	
    handleChange = (e) => {
        this.props.InputActions.setInput(e.target.value);	//setInput() : action creator
    };

    handleInsert = () => {
        const todo = {
            id: this.getId(),
            text:this.props.value,
            done:false
        };
        this.props.TodosActions.insert(todo);			//insert() : action creator	
        this.props.InputActions.setInput(' ');			//setInput() : action creator
    };


    render() {
        return (
            <div>
                <TodoInput
                    onChange={this.handleChange}
                    onInsert={this.handleInsert}
                    value={this.props.value}
                />
            </div>
        );
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(TodoInputContainer);

 

4-2. TodoListContainer 생성

import React, {Component} from 'react';
import TodoList from '../components/TodoList';

import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import * as todoActions from '../modules/todos';

const mapStateToProps = (state) => ({
    todos:state.todos
});

const mapDispatchToProps = (dispatch) => ({
    TodoActions : bindActionCreators(todoActions, dispatch)
});

class TodoListContainer extends Component {
    handleToggle = (id) => {
        this.props.TodoActions.toggle(id);  //toggle() : action creator
    }
    handleRemove = (id) => {
        this.props.TodoActions.remove(id);  //remove() : action creator
    }

    render() {
        return (
            <div>
                <TodoList
                    todos={this.props.todos}
                    onToggle={this.handleToggle}
                    onRemove={this.handleRemove}
                />
            </div>
        );
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoListContainer);

 

5. TodoList 수정

import React, { Component } from 'react';
import TodoItem from '../TodoItem';

class TodoList extends Component {
    
    render() {
        const { todos, onToggle, onRemove } = this.props;
        const todoList = todos.map((todo) => {
            return (
                <TodoItem
                    key={todo.get('id')}
                    done={todo.get('done')}
                    onToggle={() => onToggle(todo.get('id'))}
                    onRemove={() => onRemove(todo.get('id'))}
                >
                    {todo.get('text')}
                </TodoItem>
            )
        })
        return (
            <div>
                {todoList}
            </div>
        );
    }
}

export default TodoList;

 

6. App.js 수정

import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInputContainer from '../containers/TodoInputContainer';
import TodoListContainer from '../containers/TodoListContainer';


class App extends Component {

    render() {
       
        return (
            <div>
                <PageTemplate>
                    <TodoInputContainer />
                    <TodoListContainer />
                </PageTemplate>
            </div>
        );
    };
};

export default App;

'Javascript > projects' 카테고리의 다른 글

blog 1-1  (0) 2019.09.05
Counter (2) : 멀티 카운터  (0) 2019.08.29
Counter (1) : 카운터 만들기  (0) 2019.08.29
Todo-list (4) : 리렌더링 최적화 하기  (0) 2019.08.29
Todo-list (3) : 데이터 추가, 수정, 삭제  (0) 2019.08.29
Posted by yongminLEE
2019. 8. 29. 21:50

0. 디렉터리 생성

 

1. action types 수정

//src/actions/ActionTypes.js

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const SET_COLOR = 'SET_COLOR';
export const CREATE = 'CREATE';
export const REMOVE ='REMOVE';

 

2. action creators 수정

//src/actions/index.js

import * as types from './ActionTypes';

export const increment = (index) => ({
    type:types.INCREMENT,
    index
});

export const decrement = (index) => ({
    type:types.DECREMENT,
    index
    
});

export const setColor = (color, index) => ({
    type:types.SET_COLOR,
    color,
    index
});

export const create = (color) => ({
    type:types.CREATE,
    color
});

export const remove = () => ({
    type:types.REMOVE
});

 

3. reducers 수정

//src/reducers/index.js

import * as types from '../actions/ActionTypes';

const initialState = { counters: [{ color: 'black', number: 1 }] };


const reducers = (state = initialState, action) => {

    const {counters} = state;

    switch (action.type) {
        case types.CREATE:
            return {
                counters:[...counters,{color:action.color, number:1}]
            };
        case types.REMOVE:
            return {
                counters:counters.slice(0,counters.length-1)
            };
        case types.INCREMENT:
            return { 
                counters:[...counters.slice(0,action.index),
                {
                    ...counters[action.index],
                    number:counters[action.index].number+1
                },
                ...counters.slice(action.index+1, counters.length)
                ]
            };
        case types.DECREMENT:
            return { 
                counters:[...counters.slice(0,action.index),
                {
                    ...counters[action.index],
                    number:counters[action.index].number-1
                },
                ...counters.slice(action.index+1, counters.length)
                ]
            };
        case types.SET_COLOR:
            return { 
                counters:[...counters.slice(0,action.index),
                {
                    ...counters[action.index],
                    color:action.color
                },
                ...counters.slice(action.index+1, counters.length)
                ]
            };
        default:
            return state;
    }
}


 export default reducers;

 

4. Button 컴포넌트 생성

//src/components/Button.js

import React from 'react';
import PropTypes from 'prop-types';
import './Button.css';

const Button = ({onCreate, onRemove}) => {
    return (
        <div className="Button">
            <div className="btn add" onClick={onCreate}>CREATE</div>
            <div className="btn rm" onClick={onRemove}>REMOVE</div>
        </div>
    );
};

Button.propTpes = {
    onCreate:PropTypes.func,
    onRemove:PropTypes.func
};

Button.defaultProps = {
    onCreate: () => console.warn("onCreate is not defined")
}

export default Button;

 

/* src/components/Button.css */
.Button {
    display:flex;
}

.Button .btn{
    flex:1;
    display:flex;
    align-items: center;
    justify-content: center;
    height: 3rem;
    color: white;
    font-size: 1rem;
    cursor: pointer;
}

.Button .add{
    background: green;
}

.Button .add:hover{
    background: yellow;
}

.Button .rm{
    background: red;
}

.Button .rm:hover{
    background: yellow;
}

 

5. CounterList 컴포넌트 생성

//src/components/CounterList.js

import React from 'react';
import Counter from './Counter';
import PropTypes from 'prop-types';

const CounterList = ({ counters, onIncrement, onDecrement, onSetColor }) => {
    const counterList = counters.map((counter, i) => (
        <Counter
            key={i}
            index={i}
            {...counter}
            onIncrement={onIncrement}
            onDecrement={onDecrement}
            onSetColor={onSetColor} 
        />
    ));

    return (
        <div className="counterList">
            {counterList}
        </div>
    );
};

CounterList.propTypes = {
    onIncrement: PropTypes.func,
    onDecrement: PropTypes.func,
    onSetColor: PropTypes.func
};

Counter.defaultProps = {
   counters:[],
    onIncrement: () => console.warn('onIncrement function is not defined'),
    onDecrement: () => console.warn('onDecrement function is not defined'),
    onSetColor: () => console.warn('onSetColor function is not defined')
}

export default CounterList;

 

6. Counter 컴포넌트 수정

//src/components/Counter.js

import React from 'react';
import PropTypes from 'prop-types';
import './Counter.css';


const Counter = ({ number, color, index, onIncrement, onDecrement, onSetColor }) => {
    return (
        <div
            className="counter"
            onClick={()=>onIncrement(index)}
            onContextMenu={(e) => {           //마우스 오른쪽 버튼을 눌렀을 때 메뉴가 열리는 이벤트
               e.preventDefault();         //메뉴가 열리는 것을 방지
                onDecrement(index);              // => 마우스 오른쪽 클릭 : 감소
            }}
            onDoubleClick={()=>onSetColor(index)}
            style={{ backgroundColor: color }}
        >
            {number}
        </div>
    );

};

//{ number, color, onIncrement, onDecrement, onSetColor 
Counter.propTypes = {
    index: PropTypes.number,
    number: PropTypes.number,
    color: PropTypes.string,
    onIncrement: PropTypes.func,
    onDecrement: PropTypes.func,
    onSetColor: PropTypes.func
};

Counter.defaultProps = {
    index:0,
    number: 0,
    color: 'black',
    onIncrement: () => console.warn('onIncrement function is not defined'),
    onDecrement: () => console.warn('onDecrement function is not defined'),
    onSetColor: () => console.warn('onSetColor function is not defined')
}
export default Counter;

 

7. getRandomColor 라이브러리 생성

// src/lib/getRandomColor.js

const getRandomColor = () => {
    const colors = ['green', 'yellow', 'blue'];
    const random = Math.floor(Math.random() * 3);
    return colors[random];
};

export default getRandomColor;

 

8. CounterContainer 삭제, CounterListContainer 생성

// src/containers/CounterListContainer.js

import CounterList from '../components/CounterList';
import * as actions from '../actions';
import {connect} from 'react-redux';
import getRandomColor from '../lib/getRandomColor';

const mapStateToProps = (state) => ({
    counters:state.counters
});

const mapDispatchToProps = (dispatch) => ({
    onIncrement:(index)=>dispatch(actions.increment(index)),
    onDecrement:(index)=>dispatch(actions.decrement(index)),
    onSetColor:(index) => {
        const color = getRandomColor();
        dispatch(actions.setColor(color, index));
    }
});

const CounterListContainer = connect(mapStateToProps, mapDispatchToProps)(CounterList);
export default CounterListContainer;

 

9. App 컨테이너 컴포넌트 수정

// src/containers/App.js

import React, {Component} from 'react';
import CounterListContainer from './CounterListContainer';
import Button from '../components/Button';

import {connect} from 'react-redux';
import * as actions from '../actions';
import getRandomColor from '../lib/getRandomColor';

class App extends Component {
    render() {
        const {onCreate, onRemove} = this.props;
        return (
            <div className="App">
                <Button
                    onCreate={onCreate}
                    onRemove={onRemove}
                />
                <CounterListContainer />
            </div>
        );
    }
};

const mapDispatchToProps = (dispatch) => ({
    onCreate: ()=>dispatch(actions.create(getRandomColor())),
    onRemove:()=>dispatch(actions.remove())
});

export default connect(null,mapDispatchToProps)(App);

- AppContainer 안에서 App과 redux를 connect하지 않고 App 컴포넌트에서 바로 연결

- store.state값을 필요로 하지 않으므로 connect()의 ampStateToProps는 null로 설정

'Javascript > projects' 카테고리의 다른 글

blog 1-1  (0) 2019.09.05
Todo-list (5) : redux 적용  (0) 2019.09.01
Counter (1) : 카운터 만들기  (0) 2019.08.29
Todo-list (4) : 리렌더링 최적화 하기  (0) 2019.08.29
Todo-list (3) : 데이터 추가, 수정, 삭제  (0) 2019.08.29
Posted by yongminLEE
2019. 8. 29. 18:24

0. 디렉터리 생성

 

1. App.js 생성

//src/containers/App.js
import React, {Component} from 'react';

class App extends Component {
    render() {
        return (
            <div>
                Counter
            </div>
        );
    }
}

export default App;

 

2. index.js 생성

//src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './containers/App';

ReactDOM.render(<App />, document.getElementById('root'));

 

 

3. Counter 컴포넌트 생성

//src/components/Counter.js
//presentational component
// view를 담당, 리덕스 스토어에 직접 접근할 권한은 없으며 오직 props로만 데이터를 가져올 수 있음, 주로 함수형 컴포넌트로 작성

import React from 'react';
import PropTypes from 'prop-types';
import './Counter.css';


const Counter = ({ number, color, onIncrement, onDecrement, onSetColor }) => {
    return (
        <div
            className="counter"
            onClick={onIncrement}
            onContextMenu={(e) => {           //마우스 오른쪽 버튼을 눌렀을 때 메뉴가 열리는 이벤트
               e.preventDefault();         //메뉴가 열리는 것을 방지
                onDecrement();              // => 마우스 오른쪽 클릭 : 감소
            }}
            onDoubleClick={onSetColor}
            style={{ backgroundColor: color }}
        >
            {number}
        </div>
    );

};

//{ number, color, onIncrement, onDecrement, onSetColor 
Counter.propTypes = {
    number: PropTypes.number,
    color: PropTypes.string,
    onIncrement: PropTypes.func,
    onDecrement: PropTypes.func,
    onSetColor: PropTypes.func
};

Counter.defaultProps = {
    number: 0,
    color: 'black',
    onIncrement: () => console.warn('onIncrement function is not defined'),
    onDecrement: () => console.warn('onDecrement function is not defined'),
    onSetColor: () => console.warn('onSetColor function is not defined')
}
export default Counter;

onContextMenu() : 마우스 오른쪽 버튼을 눌렀을때 메뉴가 열리는 이벤트

e.preventDefault() : 메뉴가 열리는 것을 방지

/*src/components/Counter.css*/
.counter {
    width: 10rem;
    height: 10rem;
    border-radius: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 1rem;
    color: white;
    font-size: 3rem;
    cursor: pointer;
    user-select: none; /* 더블클릭을 통한 텍스트 선택 불가능 */
    transition: background-color 0.2s;
}

App.js에 Counter.js 렌더링

//src/containers/App.js
import React, {Component} from 'react';
import Counter from '../components/Counter';

class App extends Component {
    render() {
        return (
            <div>
                <Counter />
            </div>
        );
    }
}

export default App;

 

 

4. action types 생성

//src/actions/ActionTypes.js

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const SET_COLOR = 'SET_COLOR';

 

5. action creator function 생성

//src/actoins/index.js

import * as types from './ActionTypes';

export const increment = () => ({
    type:types.INCREMENT
});

export const decrement = () => ({
    type:types.DECREMENT
    
});

export const setColor = (color) => ({
    type:types.SET_COLOR,
    color:color
});

 

6. reducer 생성

//src/reducers/reducerColor.js

import * as types from '../actions/ActionTypes';

const initialState = {
    color: 'red'
};

const reducerColor = (state = initialState.color, action) => {
    switch (action.type) {
        case types.SET_COLOR:
            return {  color: action.color };
        default:
            return state;
    }
}

export default reducerColor;

 

//src/reducers/reducerNumber.js

import * as types from '../actions/ActionTypes';

const initialState = {
    number:1
}

const reducerNumber = (state=initialState, action) =>{
    switch (action.type) {
        case types.INCREMENT:
            return {number:state.number+1};
        case types.DECREMENT:
            return {number:state.number-1};
        default:
            return state;
    }
};

export default reducerNumber;

 

//src/reducers/index.js

import reducerNumber from './reducerNumber';
import reducerColor from './reducerColor';
import {combineReducers} from 'redux';

//리듀서에 의해 만들어진 state는 어플리케이션 state와 합쳐지고
const reducers = combineReducers({
    numberData : reducerNumber,
    colorData : reducerColor
});
//새로 만들어진 state는 컨테이너로 전달


export default reducers;

 

7. 스토어 생성

//src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './containers/App';

//creating store
import { createStore } from 'redux';
import reducers from './reducers';
const store = createStore(reducers, window.devToolsExtension && window.devToolsExtension());

ReactDOM.render(<App />,ndocument.getElementById('root'));

- 리엑트에서 스토어를 생성할 때는 보통 프로젝트의 엔트리 포인트인 src/index.js파일에서 만든다

 

8. Provider 컴포넌트로 App.js와 store 연동

//src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './containers/App';

//creating store
import { createStore } from 'redux';
import reducers from './reducers';
const store = createStore(reducers, window.devToolsExtension && window.devToolsExtension());

//react와 store 연동
import { Provider } from 'react-redux'; 

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);

- Provider는 react-redux 라이브러리에 내장된 리액트 app에 store를 연동시키는 컴포넌트

 

9. CounterContainer 컴포넌트 생성

//src/containers/CounterContainer.js
import Counter from '../components/Counter';
import * as actions from '../actions';
import {connect} from 'react-redux'; // subscrie

//랜덤하게 색깔 선택
export const getRandomColor = () => {
    const colors = ['green', 'yellow', 'blue'];
    const random = Math.floor(Math.random()*3);
    return colors[random];
};

const mapStateToProps = (state) => ({
    number:state.numberData.number,
    color:state.colorData.color
});

const mapDispatchToProps = (dispatch) => ({
    onIncrement: ()=>dispatch(actions.increment()),
    onDecrement: ()=>dispatch(actions.decrement()),
    onSetColor:() => {
        const color = getRandomColor();
        dispatch(actions.setColor(color));
    }
});

//Promte Counter from a component to a container
// => it needs to know about dispatch methods, which would be available as props
const CounterContainer = connect(mapStateToProps, mapDispatchToProps)(Counter);

export default CounterContainer;

- 리덕스의 buit-in-function인 subscribe 대신 react-redux 라이브러리의 connect(mapStateToProps, mapDispatchToProps, mergeProps) 메서드를 사용하여 컴포넌트를 스토어에 연결

mapStateToProps : state값을 파라미터로 받아 props로 전달하는 함수, props로 사용할 객체를 반환

mapDispatchToProps : 액션을 스토어에게 전달하는 dispatch함수를 파라미터로 받고 props에 연결하는 함수. 전달하는 함수들을 객체 안에 넣어서 반환

mergeProps :  state와 dispatch가 동시에 필요한 함수를 props에 전달할 때 사용

//src/containers.App.js
import React, {Component} from 'react';
import CounterContainer from './CounterContainer';

class App extends Component {
    render() {
        return (
            <div>
                <CounterContainer />
            </div>
        );
    }
}

export default App;
Posted by yongminLEE
2019. 8. 29. 18:09

 

 

1. event 발생 -> action creator 호출

2. action creator는 action(object) 생성

3. 생성된 action은 모든 middleware를 거쳐 모든 reducer들에게 전달

4. reducer는 action type에 따른 state반환

5. state는 app의 state에 덮어쓰이고 모든 container들에게 전달

6. 컴포넌트 리렌더링 

'Javascript > Redux' 카테고리의 다른 글

#13 Redux in React  (0) 2020.02.25
#12 Redux  (0) 2020.02.25
middleware  (0) 2019.09.02
Posted by yongminLEE
2019. 8. 29. 17:44

문제점 찾기

리액트는 부모 컴포넌트가 리랜더링 되면 자식 컴포넌트도 리랜더링 된다.

더미 데이터를 만들어 성능을 테스트하고 최적화 해야될 부분을 찾아보자.

더미 데이터 테스트하기

성능을 테스트하기 위해서 TodoList에 들어갈 TodoItem 객체 배열의 갯수를 늘려본다.

new Array() 메소드로 새로운 배열을 만들고 map을 활용하여 테스트 데이터 객체 배열을 만든다.

import (...)

const initialTodos = new Array(5000).fill(0).map(
  (foo, index) => ({id: index, text: `일정${index}`, done: false})
);

class App extends Component {

(...)

    render() {
        const { input, todos } = this.state;
        const {
            onChangeHandler,
            dataInsertHandler,
            dataRemoveHandler,
            toggleHandler,
        } = this;

        return (
            <PageTemplate>
                <InputTodo onChange={onChangeHandler} value={input} onInsert={dataInsertHandler}/>
                <TodoList todos={todos} onToggle={toggleHandler} onRemove={dataRemoveHandler}/>
            </PageTemplate>
        );
    }
}

export default App;

크롬 개발자 도구 [Performance] 탭으로 성능 측정하기

개발 서버를 실행하고 크롬 개발자 도구 Performance 탭에서 동그란 record 버튼을 누르고

새로고침을 한뒤 stop 을해보자 Timmings 부분을 확인하면 아래처럼 처리한 작업들의 기록이 시각화되어 한눈에 확인 할 수 있다.

리액트 개발자 도구 Highligh Updates

크롬 react 개발자 도구 확장 프로그램에서 Highlight Updates 를 on 시키면 리랜더링이 될때마다 시각적으로 확인할 수 있다.

문제점 찾기

인풋에 타이핑을 해보자 input만 변경되는데 모든 컴포넌트들이 리랜더링 되고 있다. 여기서 리랜더링은 실제 브라우져의 DOM의 랜더링이 아니라 Virtual DOM의 리랜더링을 의미한다. 이러한 Virtual DOM의 불필요한 리랜더링을 shouldComponentUpdate 로 방지하여야 한다.


최적화 하기

shouldComponentUpdate: false가 return 되면 랜더링 하지 않는다. 이름을 잘보자! should component update?

TodoList 컴포넌트 최적화

문제점 찾기에서 input폼의 값을 변경했는데 불필요하게 TodoList가 리랜더링 되었다.

TodoList 컴포넌트가 뭔지 잘 생각해보고 언제 TodoList가 다시랜더링 되어야 할까?

TodoList는 todos가 변경 되었을때 업데이트 되어야한다.

아래와 같이 shouldComponentUpdate 메소드에서 todos를 비교하여 다를경우만 True 를 리턴하여 리랜더링 될수 있게 코드를 추가한다.

//src/components/TodoList/TodoList.js

import React, { Component } from 'react';
import TodoItem from '../TodoItem';

class TodoList extends Component {
  shouldComponentUpdate(nextProps, nextState, nextContext) {
    return this.props.todos !== nextProps.todos;
  }

  render() {...}
}

export default TodoList;

 

더미를 5000개 두고 테스트했을때 shouldComponentUpdate 로 최적화를 시키지 않았을때는 인풋에 값을 변경하면 리랜더링 하는데 시간이 오래걸려 연속적인 타이핑이 힘들고 버벅거렸는데 최적화 코드를 적용한 후에는 다음과같이 inputTodo 컴포넌트만 0.17ms만에 업데이트 된것을 확인할 수 있다.

TodoItem 컴포넌트 최적화

이번에는 TodoItem Toggle 쪽을 살펴보자 토글(done)값이 바뀌기 때문에 TodoList가 리랜더링 되는것은 맞지만 하나의 아이템만 Toggle했는데 모든 TodoItem들이 리랜더링 되고 있다.

아래와 같이 done의 값이 다른 아이템만 true값을 반환하여 리랜더링 되도록 설정한다.

//src/components/TodoItem/TodoItem.js

import React, {Component} from 'react';
import styles from './TodoItem.scss';
import classNames from 'classnames/bind';

const cx = classNames.bind(styles);

class TodoItem extends Component {
  shouldComponentUpdate(nextProps, nextState, nextContext) {
    return this.props.done !== nextProps.done;
  }

  render() {...}
}

export default TodoItem;

 

토글을 하여 정상적으로 하나의 아이템만 리랜더링 되는지 확인해본다.


정리

리액트는 부모 컴포넌트가 리렌더링 되면 자식 컴포넌트도 리랜더링 된다.

shouldComponentUpdate를 구현해야되는 경우

  • 리스트로 컴포넌트 배열이 랜더링 되는 경우
  • 아이템 컴포넌트가 리스트 컴포넌트 내부에 있는 경우
  • 자식 컴포넌트가 많고, 리랜더링이 불필요한 상황에서 리랜더링이 될때

리스트를 랜더링 할때는 항상 shouldComponentUpdate를 구현하고 나머지의 경우 성능 최적화가 필요하다고 판단될 경우 상황에 따라 구현한다.

 

출처 : https://juicyjusung.github.io/2019/03/28/react/React-To-do-list-%EB%A7%8C%EB%93%A4%EA%B8%B0-4-%EB%A6%AC%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%95%98%EA%B8%B0/

Posted by yongminLEE