2019. 9. 5. 11:06

* react + redux-thunk + styled-component + express + mongoDB

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. 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
2019. 8. 29. 17:40

상태관리

1. Input 컴포넌트(텍스트 입력) 관련 상태 관리

InputTodo 컴포넌트의 부모 컴포넌트인 App.js 에서

  • state에 input값 정의하기
  • input 폼 변경시 변경 이벤트 메서드 생성
//src/components/App.js

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

class App extends Component {
    state = {
        input: '',
    };

    onChangeHandler = (e) => {
        const { value } = e.target;
        this.setState({
            input: value,
        });
    };

    render() {
        const { input } = this.state;
        const { onChangeHandler } = this;

        return (
            <PageTemplate>
                <InputTodo onChange={onChangeHandler} value={input} />
                <TodoList />
            </PageTemplate>
        );
    }
}

export default App;

위와같이 state  onChangeHanlder 를 설정하고 InputTodo에 props로 전달한다.

개발 서버를 실행하고 리액트 개발자 도구 React Developer Tools 를 실행하여 입력폼에 데이터가 수정될때 이벤트가 발생하여 state 값이 수정 되는지 확인한다.


 

2. 초기 데이터 정의 하고 렌더링 확인하기

이번에는 TodoList에 props로 초기 데이터를 전달하여보자. state에 todos 객체 배열을 생성하고 초기 데이터를 작성한뒤 TodoList의 props로 전달한다.

//src/components/App.js

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

class App extends Component {
    state = {
        input: '',
        todos: [
            { id: 0, text: '첫번째 일정 입니다.', done: true },
            { id: 1, text: '두번째 일정 입니다.', done: false },
        ]
    };

    onChangeHandler = (e) => {
        const { value } = e.target;
        this.setState({
            input: value,
        });
    };

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

        return (
            <PageTemplate>
                <InputTodo onChange={onChangeHandler} value={input} />
                <TodoList todos={todos} />
            </PageTemplate>
        );
    }
}

export default App;

TodoList.js 에서는 props 로 전달된 객체배열 todos를 map으로 TodoItem으로 구성된 (랜더링 문법에 맞게) 새로운 배열을 생성하고 이 데이터를 랜더링 한다.

//src/components/TodoList/TodoList.js

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

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

        return (
            <div>
                {todoList}
            </div>
        );
    }
}

export default TodoList;

개발 서버에 정상적으로 랜더링이 되는지 확인한다.


3. 데이터 추가하기 (To-do item 추가)

input폼에 데이터를 입력하고 추가 버튼을 눌렀을때 list에 추가가 될 수 있게 해보자.

추가 버튼을 누르면 input폼의 값을 기존 todos 객체배열에 추가하고 setState 메소드로 state값을 변경할 수 있는 dataInsertHandler 메서드를 정의 한다.

//src/components/App.js

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

class App extends Component {
    state = {
        input: '',
        todos: [
            { id: 0, text: '첫번째 일정 입니다.', done: true },
            { id: 1, text: '두번째 일정 입니다.', done: false },
        ]
    };

    onChangeHandler = (e) => {
        const { value } = e.target;
        this.setState({
            input: value,
        });
    };

    id = 1;
    getId = () => ++this.id;

    dataInsertHandler = () => {
        const { todos, input } = this.state;
        if (input) {
            const newTodos = [
                ...todos,
                {
                    id: this.getId(),
                    text: input,
                    done: false
                }
            ];

            this.setState({
                todos: newTodos,
                input: ''
            });
        }
    };

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

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

export default App;

객체 내부의 id값을 추가할때마다 증가시킨다.

id = 1;
getId = () => ++this.id;

추가 버튼을 눌렀을때의 핸들러로 input값이 존재할때만 추가를 하며

전개 연산자(…)로 배열을 다루어 좀더 가독성을 높인다.

dataInsertHandler = () => {
        const { todos, input } = this.state;
        if (input) {
            const newTodos = [
                ...todos,
                {
                    id: this.getId(),
                    text: input,
                    done: false
                }
            ];

            this.setState({
                todos: newTodos,
                input: ''
            });
        }
    };

추가가 잘 되나 확인한다.


4. 데이터 수정 (To-do item 완료/미완료 토글)

데이터를 수정하는 기능을 구현해 보자. TodoItem을 클릭했을때 체크 박스 활성화/비활성화를 구현한다.

  • id로 토글 원하는 데이터 찾기
  • 찾은 데이터의 객체를 수정한 새로운 객체배열 만들기
  • state값 업데이트하기
//src/components/App.js

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

class App extends Component {
    (...)

    toggleHandler = (id) => {
        const { todos } = this.state;
        const index = todos.findIndex(todo => todo.id === id);

        const toggled = {
            ...todos[index],
            done: !todos[index].done
        };

        const newTodos = [
            ...todos.slice(0, index),
            toggled,
            ...todos.slice(index + 1, todos.length)
        ];
        this.setState({
            todos: newTodos
        })

    };

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

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

export default App;

 

//src/components/TodoList/TodoList.js

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

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

        return (
            <div>
                {todoList}
            </div>
        );
    }
}

export default TodoList;

토글이 정상적으로 작동하는지 확인해본다.


5. 데이터 제거 (To-do item 삭제하기)

src/components/App.js

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

class App extends Component {
    (...)

    dataRemoveHandler = (id) => {
        const { todos } = this.state;
        const index = todos.findIndex(todo => todo.id === id);

        const newTodos = [
            ...todos.slice(0, index),
            ...todos.slice(index + 1, todos.length)
        ];
        this.setState({
            todos: newTodos
        })
    };

    toggleHandler = (id) => {
        const { todos } = this.state;
        const index = todos.findIndex(todo => todo.id === id);

        const toggled = {
            ...todos[index],
            done: !todos[index].done
        };

        const newTodos = [
            ...todos.slice(0, index),
            toggled,
            ...todos.slice(index + 1, todos.length)
        ];
        this.setState({
            todos: newTodos
        })

    };

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

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

export default App;

 

//src/components/TodoList/TodoList.js

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.id}
                done={todo.done}
                onToggle={() => onToggle(todo.id)}
                onRemove={() => onRemove(todo.id)}>
                {todo.text}
            </TodoItem>
        });

        return (
            <div>
                {todoList}
            </div>
        );
    }
}

export default TodoList;

데이터 수정과 마찬가지로 id로 해당 인덱스를 찾고 해당 인덱스의 객체를 제거한 새로운 객체배열을 생성하여 state값을 업데이트한다.

개발 서버를 실행하여 [지우기] 버튼을 눌르면 예상한것처럼 데이터가 삭제되는것이 아니라 토글이 작동하게 된다.

이와같이 부모요소와 자식요소에 onClick 이벤트가 함께 설정되어 있으면 자식 → 부모 순으로 메서드를 실행한다. 즉, 토글의 onClick 이벤트와 [지우기]의 onClick 이벤트가 겹쳐서 우리가 예상한 동작을 하지 않는것이다. 이것을 propagation 이라고 한다. 이를 방지하려면 자식 요소의 onClick 처리 함수 내부에서 e.stopPropagation 함수를 호출해주어야 한다.

//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 {
  render() {
    const {done, children, onToggle, onRemove} = this.props;

    return (
      <div className={cx('todo-item')} onClick={onToggle}>
        <input className={cx('tick')} type="checkbox" checked={done} readOnly/>
        <div className={cx('text', {done})}>{children}</div>
        <div className={cx('delete')} onClick={(e) => {
          onRemove();
          e.stopPropagation();
        }}>[지우기]</div>
      </div>
    );
  }
}

export default TodoItem;

위와같이 onClick 이벤트에 e.stopPropagation() 을 호출하여준다음에 지우기 버튼이 잘 동작하는지 확인한다.

 

출처:https://juicyjusung.github.io/2019/03/27/react/React-To-do-list-%EB%A7%8C%EB%93%A4%EA%B8%B0-3-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B6%94%EA%B0%80-%EC%88%98%EC%A0%95-%EC%82%AD%EC%A0%9C/

Posted by yongminLEE
2019. 8. 29. 17:07

1. Component 계획

다음과 같이 4개의 컴포넌트로 To-do list 를 구성 하려고 한다.

  • PageTemplate : UI의 전체 틀을 설정한다.
  • InputTodo : 일정을 추가할때 사용하는 컴포넌트이다. input 폼과 추가 버튼이 있다.
  • TodoItem : To-do list의 아이템 하나 하나가 된다.
  • TodoList : 데이터를 TodoItem 컴포넌트로 리스트를 만드는 역할을 한다.

 

2. Component 생성하기

 

3. PageTemplate

//src/components/PageTemplate.js

import React from 'react'; 
import styles from './PageTemplate.scss';
import classNames from 'classnames/bind';

const cx = classNames.bind(styles);

const PageTemplate = ({ children }) => {
    return (
        <div className={cx('page-template')}>
            <h1>To-do List</h1>
            <div className={cx('content')}> {children} </div>
        </div>)
};

export default PageTemplate;

import styles from './PageTemplate.scss'; 로 스타일을 import 한뒤 classNames 패키지로 bind한다.

To-do List의 가장 큰 기본틀을 구성하며 { children } 으로 자식요소들을 로드.

 

/*src/components/PageTemplate/PageTemplate.scss*/

@import "../../styles/utils";

.page-template {

margin-top: 5rem;

margin-left: auto;

margin-right: auto;

width: 500px;

background: white;

box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px, rgba(0,0,0,0.23);

padding-top: 2rem;



@media (max-width: 768px) {

margin-top: 1rem;

width: calc(100% - 2rem);

}

h1{

ext-align: center;

font-size: 4rem;

font-weight: 300;

margin: 0;

}

.content {

margin-top: 2rem;

}

}

 

//src/components/PageTemplate/index.js

export { default } from './PageTemplate';

/ndex.js 는 default 소스가 된다.

따라서 위와 같이 index.js 를 작성해 주면 상위 컴포넌트인 App.js에서 './PageTemplate/PageTemplate.js' 가 아닌 './PageTemplate' 로 import 할 수 있다.

 

4. InputTodo

//src/components/InputToto/InputTodo.js

import React from 'react';
import styles from './InputTodo.scss';
import classNames from 'classnames/bind';

const cx = classNames.bind(styles);

const InputTodo = ({ value, onChange, onInsert }) => {
    const handleKeyPress = (e) => {
        if (e.key === 'Enter') {
            onInsert();
        }
    };

    return (
        <div className={cx('todo-input')}>
            <input onChange={onChange} value={value} onKeyPress={handleKeyPress} />
            <div className={cx('add-button')} onClick={onInsert}>추가</div>
        </div>
    )
};

export default InputTodo;

-이 컴포너트는 props를 3개 받는다.

  • value : input 값으로 설정
  • onChange : input 내용에 변경이 있을때 사용하는 이벤트
  • onInsert : 추가 버튼을 눌렀을 때 실행하는 이벤트

-키보드 Enter 키 입력 이벤트를 위해서 handleKeyPress 메서드도 생성한다.

 

/*src/components/InputTodo/InputTodo.scss*/

@import "../../styles/utils";

.todo-input {
border-top: 1px solid $oc-gray-2;
border-bottom: 1px solid $oc-gray-2;
display: flex;
padding: 1rem;

input{
flex: 1;
font-size: 1.1rem;
outline: none;
border: none;
background: transparent;
border-bottom: 1px solid $oc-gray-4;
&:focus {
border-bottom: 1px solid $oc-cyan-6;
}
}

.add-button {
width: 5rem;
height: 2rem;
margin-left: 1rem;
border: 1px solid $oc-green-7;
color: $oc-green-7;
font-weight: 500;
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: $oc-green-7;
color: white;
}
&:active {
background: $oc-green-8;
}
}
}

 

//src/components/InputTodo/index.js

export { default } from './InputTodo'

 

5. TodoItem

일정 정보를 렌더링 하는 TodoItem 컴포넌트이다.

//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 {
    render() {
        const { done, children, onToggle, onRemove } = this.props;
        return (
            <div className={cx('todo-item')} onClick={onToggle}>
                <input className={cx('tick')} type="checkbox" checked={done} readOnly />
                <div className={cx('text', { done })}>{children}</div>
                <div className={cx('delete')} onClick={onRemove}>[지우기]</div>
            </div>
        );
    }
}

export default TodoItem;
  • done : 해당 일정이 완료했는지 체크
  • children : 자식요소 (일정 내용)
  • onToggle : 일정 완료/미완료 토글
  • onRemove : 해당 일정 삭제
  • const {done, children, onToggle, onRemove} = this.props; 비구조화 할당을 사용하여 아래에서 props를 사용할때 this.props.onToggle 과 같이 메서드 앞에 this.props 를 붙이는것을 생략할 수 있다.
/*src/components/TodoItem/TodoItem.scss*/

@import "../../styles/utils";

.todo-item {
padding: 1rem;
display: flex;
align-items: center;
cursor: pointer;
.tick {
margin-left: 1rem;
}
.text {
flex: 1;
word-break: break-all;
&.done {
text-decoration: line-through;
}
}
.delete {
margin-left: 1rem;
color: $oc-red-7;
font-size: 0.8rem;
&:hover {
color: $oc-red-5;
text-decoration: underline;
}
}
&:nth-child(odd) {
background: $oc-gray-0;
}
&:hover {
background: $oc-gray-1;
}

}

/*컴포넌트 사이에 위쪽 테두리를 설정*/
.todo-item + .todo-item {
border-top: 1px solid $oc-gray-1;
}

 

src/components/TodoItem/index.js

export { default } from './TodoItem';

6. TodoList

TodoList 컴포넌트는 데이터 배열을 컴포넌트 배열로 변환하여 렌더링 하는 역할만 한다. 데이터를 컴포넌트 배열로 변환하는 코드전에 더미 데이터를 넣어 컴포넌트가 정상적으로 렌더링 되는지 테스트 해본다.

//src/components/TodoList/TodoList.js

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

class TodoList extends Component {
    render() {
        return (
            <div>
                <TodoItem done>첫번째 할일</TodoItem>
                <TodoItem>두번째 할일</TodoItem>
            </div>
        );
    }
}

export default TodoList;

 

//src/components/TodoList/index.js

export { default } from './TodoList';

7. App.js

//src/components/App.js

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

class App extends Component {
    render() {
        return (
            <PageTemplate>
                <InputTodo />
                <TodoList />
            </PageTemplate>
        );
    }
}

export default App;

 

App.js 에서 해당 컴포넌트를 로드하고 npm run start 명령어로 개발서버를 실행해보자.

정상적으로 컴포넌트들이 렌더링 되고 UI가 구성되는것을 확인할 수 있다.

 

 

출처:https://juicyjusung.github.io/2019/03/25/react/To-do-list-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-UI-%EB%94%94%EC%9E%90%EC%9D%B8-%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%83%9D%EC%84%B1/

Posted by yongminLEE
2019. 8. 29. 16:55

1. 보일러플레이트 프로젝트 생성

$create-react-app todo-list

 

 

2. 패키지 설치

$yarn add node-sass classnames open-color

 

- node-sass   : node환경에서 libsass(c언어로 작성된 매우 빠른 sass complier)를 사용할 수 있게 함

- classnames : classNames 의 bind 기능을 사용하고 나면, 우리가 스타일을 넣을때마다 styles.를
                        붙여주는걸 생략 할 수 있음.

- open-color  : 손쉽게 색상을 골라서 사용 할 수 있는 색상 팔레트 라이브러리



3. 스타일 설정

src 아래에 스타일들을 정의할 styles 디렉토리를 생성하고, 메인이 스타일이 될 main.scss 파일과 utils.scss 파일을 생성.

// src/styles/main.scss

@import "utils";

body {

background: $oc-blue-3;

margin: 0;

}



//src/styles.utils.scss

@import "~open-color/open-color.scss";

 

 

4. App.js 컴포넌트 설정

컴포넌트의 관리를 위해 /src 아래에 /src/components 디렉토리를 생성후 App.js 파일을 생성

//src/components/App.js

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <div> To-do list </div>
        );
    }
}

export default App;

 

5.index.js 설정.

//src/index.js

import React from 'react';

import ReactDOM from 'react-dom';

import './styles/main.scss';

import App from './components/App';

import * as serviceWorker from './serviceWorker';



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

 

6. 개발서버 실행 

$yarn start
Posted by yongminLEE