Hello! React.js!

Coupang

Bali 팀

김태희(로토/roto)

React.js란 무엇인가

  • facebook이 만든 View Engine
  • MVC 패턴에서 V만 담당
  • Component Base
  • !== framework

대표적인 사용처

그외에도...

특징

  • Component Base
  • Virtual DOM
  • One way data binding
  • JSX

this context

  • React component 내에서 쓰이는 this는 인스턴스화된 component 자기 자신을 가리킴
  • 이를 위해 내부적으로 .bind(this)가 자동으로 이루어짐
  • JAVASCRIPT의 this context에 대한 이해가 필요

Component Base

  • UI의 모든 요소는 Component를 기준으로 함
  • Component 단위로 UI 요소를 만들고, 해당 Component들을 조합해서 화면을 렌더링
  • state와 props 를 통해 data가 흐름

Component Base

// es5 style
var YourComponent = React.createClass({
  render: function() {
    return (
      <div>Hi?</div>
    );
  }
});
// es6 style
class YourComponent extends React.Component {
  render() {
    return (
      <div>Hi!</div>
    );
  }
}

Virtual DOM

  • Virtual DOM Tree를 통해 화면을 다시 그릴 때 최소한의 DOM만 갱신함

One Way Binding

  • 데이터는 한 방향으로만 흐르는 것을 지향
  • Two Way Binding에 비해 손이 많이 가지만, 데이터 흐름 추적이 좀 더 용이함
  • Binding된 값들은 해당 값들이 변하면 자동으로 해당 부분을 다시 그린다.

JSX

  • JAVASCRIPT 내에서 XML을 사용하는 문법
  • React에선 JSX 를 이용해 UI를 표현
  • XML이므로 HTML 보다 엄격하며 DOM API 기반
  • 브라우저가 알아먹게 하기 위해 babel 등을 이용해서 tranpiling

JSX

var HelloMessage = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});

ReactDOM.render(<HelloMessage name="John" />, mountNode);
"use strict";

var HelloMessage = React.createClass({
  displayName: "HelloMessage",

  render: function render() {
    return React.createElement(
      "div",
      null,
      "Hello ",
      this.props.name
    );
  }
});

ReactDOM.render(React.createElement(HelloMessage, { name: "John" }), mountNode);

Transpiling

JSX 특징

  • 모든 태그는 닫혀야 한다.
// 자주하는 실수
<br>
<img src="bali.png">

// 올바른 예
<br/>
<img src="bali.png"/>

JSX 특징

  • attribute는 camelCase로 작성해야한다.
// 잘못된 예
<table cellpadding="5">
  <tr rowspan="2">
  ...
  </tr>
</table>

// 올바른 예
<table cellPadding="5">
  <tr rowSpan="2">
  ...
  </tr>
</table>

JSX의 특징

// 틀린 예

return (
  <div>안녕</div>
  <div>세상</div>
);

// 올바른 예
return (
  <div>
    <div>안녕</div>
    <div>세상</div>
  </div>
);

최상위에는 단일 Root Node가 있어야 한다.

직접 작성해봅시다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello React</title>
    <script src="https://fb.me/react-15.2.1.js"></script>
    <script src="https://fb.me/react-dom-15.2.1.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/babel">
      // 이곳에 코드를 작성합니다.
    </script>
  </body>
</html>

Component 정의하기

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello React</title>
    <script src="https://fb.me/react-15.2.1.js"></script>
    <script src="https://fb.me/react-dom-15.2.1.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/babel">
      // HelloWorld Component 정의
      var HelloWorld = React.createClass({

        // Component는 render 함수를 기준으로 화면에 그려진다.
        render: function () {
          return (
            <div>Hello World!!!</div>
          );
        }
      });
    </script>
  </body>
</html>

React DOM Mounting

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello React</title>
    <script src="https://fb.me/react-15.2.1.js"></script>
    <script src="https://fb.me/react-dom-15.2.1.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/babel">
      // HelloWorld Component 정의
      var HelloWorld = React.createClass({
        render: function () {
          return (
            <div>Hello World!!!</div>
          );
        }
      });

      // HelloWorld Comopnent 마운팅하기
      ReactDOM.render(
        <HelloWorld />,
        document.getElementById('example')
      );
    </script>
  </body>
</html>

Component 쪼개고 조립하기

// Hello Component 정의
var Hello = React.createClass({
  render: function (){
    return (
      <span>Hello!!</span>
    );
  }
});

// World Component 정의
var World = React.createClass({
  render: function (){
    return (
      <span>World!!</span>
    );
  }
});

// Hello와 World Component를 조합한 새로운 Component
var ThreeHelloOneWorld = React.createClass({
  render: function (){
    return (
      <div>
        <Hello />
        <Hello />
        <Hello />
        <World />
      </div>
    )
  }
});

ReactDOM.render(
  <ThreeHelloOneWorld />,
  document.getElementById('example')
);

state & props

Component State

  • Component의 자기자신의 상태에 대한 값은 state로 정의한다.
  • setState를 통해 자신의 상태를 변경하면, 해당 상태 기준으로 화면이 다시 그려진다.

Component State

// Timer Component 정의
var Timer = React.createClass({
  // Component의 state를 정의하는 함수
  getInitialState: function(){
    return {
      count: 0
    };
  },
  // Component가 화면에 Mount 되면 실행되는 Life cycle 함수
  componentDidMount: function(){
    setInterval(this.tick, 1000);
  },

  // setState를 통해 상태를 갱신하는 함수
  tick: function() {
    this.setState({
      count: this.state.count + 1
    });
  },  

  render: function () {
    return (
      <div>count {this.state.count}</div>
    );
  }
});


ReactDOM.render(
  <Timer />,
  document.getElementById('example')
);

Component Props

  • Component 렌더링 시 렌더링하는 측에서 렌더링할 Component에 attribute 형태로 넘겨주는 값들이다.
  • props를 받아서 사용하는 쪽에선 props를 고치면 안 된다.
  • props의 기준이 되는 데이터가 변경되는 경우, 연결된 props도 모두 갱신이 되며 해당 Component가 다시 그려진다.

Component Props

var TeamCard = React.createClass({
  render: function () {
    return (
      <div>
        {this.props.teamName} 팀은 {this.props.floor}층에 있어요.
      </div>
    );
  }
});

var TeamCards = React.createClass({
  render: function () {
    return (
      <div>
        <TeamCard teamName="발리" floor={5} />
        <TeamCard teamName="티켓" floor={5} />
        <TeamCard teamName="아발론" floor={6} />
      </div>
    )
  }
});

// 마운팅하기
ReactDOM.render(
  <TeamCards />,
  document.getElementById('example')
);

state & props

// Timer Component 정의. props로 initialCount를 받아 자신의 state에 설정
var Timer = React.createClass({
  getInitialState: function () {
    return {
      count: this.props.initialCount
    };
  },
  componentDidMount: function (){
    setInterval(this.tick, 1000);
  },
  tick: function() {
    this.setState({
      count: this.state.count + 1
    });
  },
  render: function (){
    return (
      <div>
        <TimerText count={this.state.count} />
      </div>
    )
  }
});

// count를 props로 받아 화면에 뿌려주는 역할
var TimerText = React.createClass({
  render: function (){
    return (
      <span>현재 Count는 {this.props.count} 이다!</span>
    );
  }
});

// Timer를 렌더링하면서 props로 initialCount를 넘겨줌
ReactDOM.render(
  <Timer initialCount={105}/>,
  document.getElementById('example')
);

propTypes

  • Component 내에 propTypes를 통해 넘어올 propTypes의 형태를 정의할 수 있다.
  • 추후 코드 가독성에 매우 도움
  • type checking을 통해 버그 사전 방지

propTypes

var Timer = React.createClass({
  propTypes: {
    initialCount: React.PropTypes.number
  },
  ......
});

var TimerText = React.createClass({
  propTypes: {
    count: React.PropTypes.number.isRequired
  },
  ....
});

propTypes

React.createClass({
  propTypes: {
    // You can declare that a prop is a specific JS primitive. By default, these
    // are all optional.
    optionalArray: React.PropTypes.array,
    optionalBool: React.PropTypes.bool,
    optionalFunc: React.PropTypes.func,
    optionalNumber: React.PropTypes.number,
    optionalObject: React.PropTypes.object,
    optionalString: React.PropTypes.string,
    optionalSymbol: React.PropTypes.symbol,

    // Anything that can be rendered: numbers, strings, elements or an array
    // (or fragment) containing these types.
    optionalNode: React.PropTypes.node,

    // A React element.
    optionalElement: React.PropTypes.element,

    // You can also declare that a prop is an instance of a class. This uses
    // JS's instanceof operator.
    optionalMessage: React.PropTypes.instanceOf(Message),

    // You can ensure that your prop is limited to specific values by treating
    // it as an enum.
    optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),

    // An object that could be one of many types
    optionalUnion: React.PropTypes.oneOfType([
      React.PropTypes.string,
      React.PropTypes.number,
      React.PropTypes.instanceOf(Message)
    ]),

    // An array of a certain type
    optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),

    // An object with property values of a certain type
    optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),

    // An object taking on a particular shape
    optionalObjectWithShape: React.PropTypes.shape({
      color: React.PropTypes.string,
      fontSize: React.PropTypes.number
    }),

    // You can chain any of the above with `isRequired` to make sure a warning
    // is shown if the prop isn't provided.
    requiredFunc: React.PropTypes.func.isRequired,

    // A value of any data type
    requiredAny: React.PropTypes.any.isRequired,

    // You can also specify a custom validator. It should return an Error
    // object if the validation fails. Don't `console.warn` or throw, as this
    // won't work inside `oneOfType`.
    customProp: function(props, propName, componentName) {
      if (!/matchme/.test(props[propName])) {
        return new Error(
          'Invalid prop `' + propName + '` supplied to' +
          ' `' + componentName + '`. Validation failed.'
        );
      }
    },

    // You can also supply a custom validator to `arrayOf` and `objectOf`.
    // It should return an Error object if the validation fails. The validator
    // will be called for each key in the array or object. The first two
    // arguments of the validator are the array or object itself, and the
    // current item's key.
    customArrayProp: React.PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
      if (!/matchme/.test(propValue[key])) {
        return new Error(
          'Invalid prop `' + propFullName + '` supplied to' +
          ' `' + componentName + '`. Validation failed.'
        );
      }
    })
  },
  /* ... */
});

propTypes

propTypes에 선언된 타입과 props가 다른 경우..

propTypes

isRequired로 선언된 props를 누락한 경우..

Event System

  • UI Interaction이 일어나는 곳에 onXXX 형태로 Event를 bind
  • es5 style 기준으로 이벤트 핸들러에 this가 자동으로 bind 됨

Event System

var ClickCounter = React.createClass({
  getInitialState: function() {
    return {
      clickCounter: 0
    };
  },
  render: function () {
    return (
      <div>
        현재 버튼은 {this.state.clickCounter} 번 눌렸습니다.
        <button onClick={this.handleClick}>눌러봅시다!</button>
      </div>
    )
  },
  handleClick: function(){
    this.setState({
      clickCounter: this.state.clickCounter + 1
    });
  }
});

ReactDOM.render(<ClickCounter/>, document.getElementById('example'));

이제 초간단 Todo App을 만들어봅시다.

Component로 생각하기

  • TodoApp
    • TodoText
    • TodoList

우선...TodoApp의 구조

var TodoApp = React.createClass({
  getInitialState: function () {
    return {
      todos: [
        {
          todoText: '퇴근하기'
        },
        {
          todoText: '퇴근이..진다..'
        }
      ]
    };
  },
  render: function () {
    return (
      <div>
        <TodoText />
        <TodoList todos={this.state.todos}/>
      </div>
    );
  }
});

그리고 TodoText의 구조

var TodoText = React.createClass({
  render: function () {
    return (
      <form>
        <input type="text"
               placeholder="할 일을 입력하세요."/>
        <button type="submit">추가하기</button>
      </form>
    );
  }
});

TodoList의 구조

var TodoList = React.createClass({
  propTypes: {
    todos: React.PropTypes.array
  },
  render: function () {
    var todoLiComponents = [];
    var todos = this.props.todos;
    todos.forEach(function(todo, i){
      todoLiComponents .push(
        <li key={i}>
          {todo.todoText}
        </li>
      );
    });

    return (
      <ul>
        {todoLiComponents }
      </ul>
    );
  }
});

일단 렌더링은 성공함

TodoText의 input 처리

var TodoText = React.createClass({
  getInitialState: function () {
    return {
      todoText: ''
    };
  },
  render: function () {
    return (
      <form>
        <input type="text"
               placeholder="할 일을 입력하세요."
               value={this.state.todoText}
               onChange={this.handleTodoTextChange}/>
        <button type="submit">추가하기</button>
      </form>
    )
  },
  handleTodoTextChange: function(e) {
    this.setState({
      todoText: e.target.value
    });
  }
});

TodoText의 submit 처리

var TodoText = React.createClass({
  propTypes: {
    onAddTodo: React.PropTypes.func.isRequired
  },
  getInitialState: function () {
    return {
      todoText: ''
    };
  },
  render: function () {
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="text"
               placeholder="할 일을 입력하세요."
               value={this.state.todoText}
               onChange={this.handleTodoTextChange}/>
        <button type="submit">추가하기</button>
      </form>
    )
  },
  handleTodoTextChange: function(e) {
    this.setState({
      todoText: e.target.value
    });
  },
  handleSubmit: function(e) {
    e.preventDefault();
    this.props.onAddTodo(this.state.todoText);
    this.setState({
      todoText: ''
    });
  }
});

TodoApp에서 onAddTodo 추가하기

var TodoApp = React.createClass({
  getInitialState: function () {
    return {
      todos: [
        {
          todoText: '퇴근이..진다..'
        },
        {
          todoText: '발리 여행가기'
        }
      ]
    };
  },
  render: function () {
    return (
      <div>
        <TodoText onAddTodo={this.handleAddTodo}/>
        <TodoList todos={this.state.todos} />
      </div>
    )
  },
  handleAddTodo: function(todoText) {
    // 기존 state의 todos 를 clone
    var newTodos = this.state.todos.slice();
    newTodos.push({
      todoText: todoText
    });

    this.setState({
      todos: newTodos
    });
  }
});

todos를 ajax로 fetch해오기

var TodoApp = React.createClass({
  getInitialState: function () {
    return {
      todos: []
    };
  },
  componentDidMount: function() {
    $.get('http://demo4539895.mockable.io/todo')
      .done(function(result){
         this.setState({
           todos: result
         });
      }.bind(this));
  },
  render: function () {
    return (
      <div>
        <TodoText onAddTodo={this.handleAddTodo}/>
        <TodoList todos={this.state.todos} />
      </div>
    )
  },
  handleAddTodo: function(todoText) {
    // 기존 state의 todos 를 clone
    var newTodos = this.state.todos.slice();
    newTodos.push({
      todoText: todoText
    });

    this.setState({
      todos: newTodos
    });
  }
});

todo에 새로운 필드를 추가해봅시다.

isCompleted를 추가하기

var TodoApp = React.createClass({
  ...
  render: function () {
    return (
      <div>
        <TodoText onAddTodo={this.handleAddTodo}/>
        {// onCompleted props 추가 }
        <TodoList todos={this.state.todos} 
                  onCompleted={this.handleCompleted}/>
      </div>
    )
  },
  handleCompleted: function(index) {
    // props로 넘어간 이 핸들러가 호출되면서 파라메터로 완료처리할 todo의 index를 넘긴다
    // clone 후 값을 바꾸고 setState
    var newTodos = this.state.todos.slice();
    newTodos[index].isCompleted = true;
    // state가 바뀌면서 다시 화면이 그려짐
    this.setState({
      todos: newTodos
    });
  }
  ...
});

isCompleted를 추가하기

var TodoList = React.createClass({
  propTypes: {
    todos: React.PropTypes.arrayOf(React.PropTypes.shape({
      todoText: React.PropTypes.string,
      isCompleted: React.PropTypes.bool
    })).isRequired,
    onCompleted: React.PropTypes.func.isRequired
  },
  render: function () {
    var todoLis = [];
    var todos = this.props.todos;
    todos.forEach(function(todo, i){
      // todo의 isCompleted 값에 따라 동적으로 completed className을 설정
      todoLis.push(
        <li key={i} 
            className={todo.isCompleted ? 'completed' : ''} 
            onClick={this.handleClick}>
          {todo.todoText}
        </li>
      )
    }.bind(this));

    return (
      <ul>
        {todoLis}
      </ul>
    )
  },
  // todo 클릭 시 이벤트 핸들러. 이벤트가 일어난 지점의 index를 구해서 props의 onCompleted Callback 호출
  handleClick: function (e) {
    this.props.onCompleted($(e.target).index());
  }
});

새로운 Component를 추가하기

var TodoStatus = React.createClass({
  propTypes: {
    todos: React.PropTypes.array.isRequired
  },
  render: function (){
    var todos = this.props.todos;
    var completedCount = todos.filter(function(todo){
      return todo.isCompleted;
    }).length;
    return (
      <div>
        {todos.length} 개의 할 일 중에 {completedCount}개가 완료됨
      </div>
    )
  }
});

Todo의 상태를 보여주는 TodoStatus Component 추가

새로운 Component를 추가하기

...
render() {
  return (
    <div>
      <TodoText onAddTodo={this.handleAddTodo}/>
      <TodoList todos={this.state.todos} onCompleted={this.handleCompleted}/>
      <TodoStatus todos={this.state.todos} />
    </div>
  )
}
...

TodoApp render 에 새 Component 추가

ES6 Style

  • lexical scope
  • class style
  • arrow function
  • string template
  • and more...

 ES6 Style Component

class HelloES6React extends React.Component {
  render() {
    const {name} = this.props;

    return (
      <div>hi!!!{name}</div>
    )
  }
}

HelloES6React.propTypes = {
  name: React.PropTypes.string.isRequired
};


ReactDOM.render(<HelloES6React name="로토"/>, document.getElementById('example'));

ES6 Style TodoApp

class TodoApp extends React.Component {
  constructor() {
    super();
    this.state = {
      todos: []
    };
  }

  componentDidMount () {
    $.get('http://demo4539895.mockable.io/todo')
      .done((result) => {
         this.setState({
           todos: result
         });
      });
  }

  render() {
    return (
      <div>
        <TodoText onAddTodo={this.handleAddTodo.bind(this)}/>
        <TodoList todos={this.state.todos} onCompleted={this.handleCompleted.bind(this)}/>
      </div>
    )
  }

  handleAddTodo (todoText) {
    // 기존 state의 todos 를 clone
    var newTodos = this.state.todos.slice();
    newTodos.push({
      todoText: todoText
    });

    this.setState({
      todos: newTodos
    });
  }
}

ES6 주의점

  • ES5 Style에서 해줬던 auto binding이 없음
  • state 선언은 constructor 구문에서
  • Event Binding하는 쪽에서 this context에 대해 binding하는 처리를 해줘야 함
    http://egorsmirnov.me/2015/08/16/react-and-es6-part3.html
  • propsTypes는 Component class 선언 이후 넣어줘야 함

ES6 + ES7

ES6 + ES7 Style

class TodoApp extends React.Component {
  // constructor 생략
  state = {
    todos: []
  }

  componentDidMount () {
    $.get('http://demo4539895.mockable.io/todo')
      .done((result) => {
         this.setState({
           todos: result
         });
      });
  }

  render() {
    return (
      <div>
        <TodoText onAddTodo={this.handleAddTodo}/>
        <TodoList todos={this.state.todos}
                  onCompleted={this.handleCompleted}/>
      </div>
    )
  }

  handleAddTodo = (todoText) => {
    // 기존 state의 todos 를 clone
    var newTodos = this.state.todos.slice();
    newTodos.push({
      todoText: todoText
    });

    this.setState({
      todos: newTodos
    });
  };

class TodoList extends React.Component {
  static propTypes = {
    todos: React.PropTypes.array,
    onCompleted: React.PropTypes.func.isRequired
  };
  
  render () {
    var todoLis = [];
    var todos = this.props.todos;
    todos.forEach((todo, i) => {
      todoLis.push(
        <li key={i}
            className={todo.isCompleted ? 'completed' : ''}
            onClick={this.handleClick}>
          {todo.todoText}
        </li>
      )
    });

    return (
      <ul>
        {todoLis}
      </ul>
    )
  }

  handleClick = (e) => {
    this.props.onCompleted($(e.target).index());
  };
}

Q & A 

and more

감사합니다.

Hello React.js

By 김태희(로토/Roto) [Travel Systems CX] ­