리액트 Advanced guides 후반부 노트

😢 이 페이지는 다음 주소로 변경될 예정입니다.

리액트의 Advanced guides 페이지를 따라하면서 노트한 내용이다. 가이드 쪽은 옴니버스 같은 기분이라서 반반으로 나눠 읽기로 했다. 기록하고 싶은 부분만 남겼기 때문에 자세한 내용은 각 페이지를 참고한다.

Reconciliation

React는 선언형 API를 사용하고 있어서 변경에 대해 일일이 신경쓰지 않아도 된다. 이 가이드에서는 React가 어떤 비교 알고리즘을 사용해서 고성능을 내는지 설명한다.

모든 컴포넌트를 다 새로 그리면 O(n3)인데 다음 두 가정으로 발견적(휴리스틱, heuristic) O(n) 알고리즘을 사용한다. 대부분의 경우는 이 가정에 문제가 없다.

  1. 다른 타입의 두 엘리먼트는 다른 트리를 만듬
  2. 개발자가 key로 힌트를 제공해서 자식 엘리먼트가 반복되는 렌더링에서 안정된 상태인걸 확인할 수 있음

엘리먼트가 갱신될 때, 어떤 식으로 갱신이 발생하는가는 다음과 같다.

  • 다른 타입의 엘리먼트인 경우, 트리 전체를 다시 그림 (언마운트 && 마운트 발생)
  • 동일 타입의 DOM 엘리먼트의 경우, 어트리뷰트만 갱신함, 어트리뷰트 일부만 갱신된 경우 변경된 부분만 갱신 (e.g. stylefontWeight)
  • 동일 타입의 컴포넌트 엘리먼트의 경우, 인스턴스가 유지되며 state도 보존됨. 대신 하위 컴포넌트에는 변경 사항을 componentWillReceiveProps()componentWillUpdate()로 전파함.

자식노드가 갱신될 때는 신경써야 한다.

// 1.
<ul>
  <li>Edward</li>
</ul>

// 2. 뒤로 추가되는 경우에는 기존 엘리먼트가 유지됨
<ul>
  <li>Edward</li>
  <li>Mindy</li>
</ul>

// 3. 앞으로 추가되는 경우에는 노드 전체를 다시 그림
//    당연히 성능 하락 발생하며 컴포넌트 엘리먼트 경우
//    언마운트 마운트하게 된다
<ul>
  <li>Mindy</li>
  <li>Edward</li>
</ul>

// 4. 앞서의 가정 2에 따라서 `key`를 제공하면
//    새로 그리지 않고 반영할 수 있게 됨
<ul>
  <li key="1029">Edward</li>
</ul>

<ul>
  <li key="2012">Mindy</li>
  <li key="1029">Edward</li>
</ul>

id가 없다면 적당히 hash를 생성해서 쓴다. 동일 계층에서만 유일값을 가지면 된다. 최후의 수단은 배열의 index인데 배열 순서가 바뀌지 않는다는 가정이 있어야 한다. 순서가 바뀌면 key를 써도 느리다. 별로 권장하지 않는다.

최종적인 결과는 동일하지만 어떻게 구현되어 있는지 아는 것으로 성능 향상을 할 수 있다. 휴리스틱에 기반한 알고리즘이라서 다음 경우엔 좋지 않다.

  1. 하위 트리의 컴포넌트가 일치하는지 검사하지 않는다. 두 컴포넌트가 비슷한 결과를 낸다면 하나로 만드는걸 고려한다.
  2. 키는 안정적이고 예측 가능하며 유일해야 한다. 이 규칙을 지키지 않으면 성능 하락이나 자식 컴포넌트가 state를 잃어버리는 경우가 발생한다.

Context

props로 일일이 내려주기 번거로울 때 contextTypes로 지정하면 하위 트리에 전역으로 전달된다. prop-types가 필요하다. 흑마법이므로 사용하지 말 것. 실험적인 API로 차후 문제가 될 가능성이 높다. 문서 내내 쓰지 말라는 말이 반복된다.

Fragments

엘리먼트를 반환할 때 컨테이너 역할을 하는 <div> 등을 쓰기 마련인데 테이블 같은 걸 조립할 때는 마크업에 맞지 않다. 이런 경우를 위해 정말 컨테이너 역할만 하는 <React.Fragment>를 사용할 수 있다.

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

약식 표기로 <>, </>를 사용할 수 있지만 현재 툴에서 지원하지 않을 수도 있으니 위 방식으로 사용한다.

<React.Fragment>로 묶을 때 key도 지정할 수 있다.

function TodoList(props) {
  return (
    <dl>
    {props.items.map(item => (
      <React.Fragment key={item.id}>
        <dt>{item.thing}</dt>
        <dd>{item.done}</dd>
      </React.Fragment>
    ))}
    </dl>
  );
}

Portals

컴포넌트를 부모 컴포넌트의 DOM 트리 바깥에 붙일 때 사용하는 방법이다. 전체화면에 모달 띄우기 이런 동작 필요할 때 쓴다. 아래 코드에서 child는 렌더링 가능한 리엑트 엘리먼트고 container는 DOM 엘리먼트다.

ReactDOM.createPortal(child, container)

DOM에서 이벤트 전파(Event bubbling)는 실제 노드를 타고 올라가는 식이지만 포털은 다른 DOM 노드에 위치하고 있더라도 React 컴포넌트의 노드 트리를 타고 전파된다. DOM의 바인딩만 트리 바깥에서 일어나고 실제 모든 컴포넌트는 기존과 동일한 방식으로 동작한다.

에러 바운더리

기존엔 에러가 발생하면 컴포넌트 트리가 멍텅구리 되었는데 에러 바운더리를 사용해서 해결할 수 있다. 오류가 발생했을 때 동작을 componentDidCatch(error, info)에 선언한다. error는 발생한 오류고 infocomponentStack 키를 포함한 객체로 오류 발생 시, 스택 정보를 제공한다.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // 오류 발생 UI 표시
    this.setState({ hasError: true });
    // 로그로 남김
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

에러 바운더리는 다음처럼 컨텍스트 바깥에서 발생하는 오류는 잡지 못한다.

  • 이벤트 핸들러
  • 비동기 코드
  • 서버측 렌더링
  • 하위 컴포넌트가 아닌 에러 바운더리 자체에서 발생한 오류

react 16부터는 에러 바운더리로 에러가 잡히지 않았을 때 리엑트 컴포넌트 트리 전체를 언마운트한다. 문제 있는 UI를 그대로 두면 사용자의 잘못된 조작을 야기할 수 있기 때문이라고 한다.

create react app 사용하면 스택 추적에서 어디서 오류가 났는지 명확히 보인다. create react app을 사용하지 않는다면 플러그인을 설치하면 된다. 다만 프로덕션에서는 꺼야 한다.

try ... catch는 명령행 코드에서만 동작한다. 이벤트 핸들러의 에러는 이벤트 바운더리에서 잡지 못한다. 이벤트 핸들러의 에러는 try ... catch를 사용한다.

15에서는 unstable_handleError였다고 한다.

웹 컴포넌트

웹 컴포넌트를 react 내에서 사용할 경우에는 일반 DOM 컴포넌트를 사용하는 것처럼 쓸 수 있다. 웹 컴포넌트는 명령형 API를 쓰는 경우가 종종 있는데 ref로 참조를 받아와 DOM 노드를 직접 호출해야 할 수도 있다. 또한 웹 컴포넌트에서 발생한 이벤트가 리액트 컴포넌트에 제대로 전이되지 않을 수 있으므로 직접 핸들러를 연결해야 할 수도 있다.

웹 컴포넌트에서 React 컴포넌트를 사용하려면 ReactDOM.render직접 렌더링 해야한다.

고차 컴포넌트

고차 컴포넌트는 컴포넌트를 입력 받아 새로운 컴포넌트를 반환하는 함수를 의미한다. (고차 함수와 같은 접근 방식이다.) Redux의 connect, Relay의 createFragmentContainer도 동일한 방식이다.

function withSubscription(WrappedComponent, selectedData) {
  return class extends React.Component {
    // ...
  }
}

고차 컴포넌트 내에서 기존 컴포넌트를 변경하지 않도록 주의한다. 고차 컴포넌트 내에서 기존 컴포넌트를 변경하면 추상성이 무너진다. 순수 함수처럼 작성해야 한다. 이 접근 방식은 책임을 분리한다는 점에서 컨테이너 컴포넌트와 유사한 점이 있다.

render() {
  // 불필요한 prop을 제거하거나 추가적으로 필요한 prop을 전달한다.
  const { extraProp, ...passThroughProps } = this.props;
  const injectedProp = someStateOrinstanceMethod;
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

compose 같은 합성 함수를 사용하면 편리하다. lodashRedux, Ramda에서도 제공한다.

고차 컴포넌트에서 반환하기 전에 displayName을 추가하면 디버그를 쉽게 할 수 있다.

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component { /* ... */ };
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

고차 컴포넌트에서 지켜야 할 사항이 있다.

  1. render() 내에서는 고차 컴포넌트를 사용하지 않는다. 렌더링 할 때마다 새로운 컴포넌트를 만들고 최악의 경우 노드의 모든 상태를 잃게 된다.
  2. 고차 컴포넌트를 만들 때는 정적 메소드도 직접 복사해야 한다. hoist-non-react-statics 같은 패키지를 사용해도 된다.
  3. 고차 컴포넌트는 ref를 전달하지 못한다. ref는 일반 prop이 아니기 때문인데 부모 컴포넌트에 ref 노출하는 식으로 별도의 prop을 만들어서 전달해야 한다. 깔끔한 해결책은 아니다.

Render Props

Render props은 prop에 엘리먼트를 반환하는 함수를 전달해서 재사용성을 높이는 방법이다. React Routerdownshift에서 사용하는 방식이라고 한다.

<Mouse render={mouse => (
  <Cat mouse={mouse} />
)}/>

구체적으로 구현하는 방식보다 동작을 사용자에게 위임하는 방식으로 구현하는 접근법으로 Mouse 커서 위치를 전달하는 예를 들었다.

이런 방식으로 사용하는걸 render props라고 하지만 꼭 props가 render일 필요는 없다. 패턴 이름일 뿐이다.

설명에는 ShallowEquals 때문에 React.PureComponent에서는 익명함수가 계속 새로 생성된다고 나오는데 내가 테스트를 제대로 못하는건지 React.Component에서 하는거랑 차이가 없어 보인다. 여튼 익명함수를 반복해서 생성하고 싶지 않다면 익명함수 대신 선언한 메소드를 전달해주는 방식으로 해결할 수 있다.

constructor(props) {
  super(props);
  this.renderTheCat = this.renderTheCat.bind(this);
}
// ...
renderTheCat(mouse) {
  return <Cat mouse={mouse} />;
}
// ...
return <Mouse render={this.renderTheCat} />;

다른 라이브러리와 함께 사용하기

DOM을 직접 제어하는 플러그인과 함께 사용하려면 ref로 DOM 엘리먼트를 노출하고 직접 제어한다.

componentDidMount() {
  this.$el = $(this.el);
  this.$el.chosen();
}

componentWillUnmount() {
  this.$el.chosen('destroy');
}
render() {
  return (<select ref={el => this.el = el}>{this.props.children}</select>);
}

다른 뷰 라이브러리와 연동하기에서는 리액트로 포팅하는 방법이랑 Backbone.View에 리액트 컴포넌트를 어떻게 넣는지 설명한다.

React state, flux, redux를 권하긴 하지만 모델 레이어와도 통합이 가능하다.

Backbone의 모델을 컴포넌트에서 사용하려면 backbone에서 사용하는 방식대로 사용하면 되고 Backbone의 모델에서 데이터를 가져오는 방식으로는 고차 컴포넌트 형태로 활용할 수 있다.

설명은 Backbone으로 했지만 여기서 사용한 기법 자체는 제한적이지 않다.

접근성

WAI-ARIA 적용, 시멘틱 HTML, 폼 접근성, 포커스 컨트롤 등 접근성 관련된 내용을 설명하는데 읽어봐야 하는 문서를 전부 나열하고 있다. 리액트에 특정한 부분이 아니라서 각 링크는 본문을 확인한다.

// for 대신 htmlFor를 사용
<label htmlFor="namedInput">Name:</label>
<input id="namedInput" type="text" name="name"/>

포커스 제어는 ref로 직접 DOM을 받아서 처리한다.

코드 분할

import() 등 nodejs에서 사용하는 일반적인 코드 분할 기법을 설명한다.

리액트 Advanced guides 전반부 노트

😢 이 페이지는 다음 주소로 변경될 예정입니다.

리액트의 Advanced guides 페이지를 따라하면서 노트한 내용이다. 가이드 쪽은 옴니버스 같은 기분이라서 반반으로 나눠 읽기로 했다. 기록하고 싶은 부분만 남겼기 때문에 자세한 내용은 각 페이지를 참고한다.

JSX in Depth

리액트 엘리먼트 타입 정의

JSX는 React.createElement(component, props, ...children)의 편의 문법이다. 그래서 JSX를 사용할 때는 스코프 내에 React가 꼭 필요하다.

다음처럼 점 표기법을 사용할 수 있다. 한번에 여러 컴포넌트 내보낼 때 편리하다.

const MyComponents = {
  DatePicker: function DatePicker(props) {
    return <div>Imagine a {props.color} datepicker here.</div>;
  }
}

function BlueDatePicker() {
  return <MyComponents.DatePicker color="blue" />;
}

사용자 정의 컴포넌트는 꼭 Capitalized 되어야 한다. 소문자로 된 컴포넌트라면 사용하기 전에 Capitalized 하는 방식으로 사용할 수 있다. 동적으로 사용할 때도 이런 방식으로 사용한다.

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(Props) {
  // Wrong
  return <components[props.storyType] story={props.story} />;

  // Correct
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

Props

아래는 각각 동일한 표현이다.

// 문자열 리터럴
<MyComponent message="hello world" />
<MyComponent message={'hello world'} />

// 문자열 리터럴은 HTML-unescaped로 처리됨
<MyComponent message="&lt;3" />
<MyComponent message={'<3'} />

// Prop의 기본 값은 `True`
<MyComponent autocomplete />
<MyComponent autocomplete={true} />

Spread Attribute로 간편하게 표현할 수 있다.

<Greeting firstName="John" lastName="Dorian" nickName="Bambi" />

const props = {firstName: 'John', lastName: 'Dorian', nickName: 'Bambi'};
<Greeting {...props} />

const { nickName, ...other } = props;
const nick = nickName === 'Bambi' ? 'Newbie' : 'Scooter';
<button nickName={nick} {...other} />

자식 노드

문자열은 문자열로 처리되고 개행은 공백으로 처리된다.

render()에서 배열로 반환하면 합쳐서 렌더링한다.

JS 표현식도 자식 노드에 사용할 수 있다. 배열도 렌더링 하기 때문에 다음처럼 쓸 수 있다.

<ul>
  {todos.map((message) => <Item key={message} message={message} />)}
</ul>

children에 함수도 전달할 수 있다. Lifting state up이랑 비슷한 느낌이다. 세부 구현을 사용자에게 위임할 수 있을 것 같다.

function Repeat(props) {
  let items = [];
  for (let i = 0; i < props.numTimes; i++) {
    items.push(props.children(i));
  }
  return <div>{items}</div>;
}

function TodoList() {
  const todos = ['finish doc', 'submit pr', 'review'];
  return (
  <Repeat numTimes={10}>
    {(index) => <div key={index}>This is item {index} in the list</div>}
  </Repeat>
  );
}

Boolean, null, undefined는 화면에 렌더링하지 않는다.

// 조건부 표현
{showHeader && <Header />}

// false가 아닌 falsy한 값을 반환하는 경우에는 렌더링되는 점을 주의, 명확하게 boolean으로 반환할 것
{props.messages.length > 0 && <MessageList messages={props.messages} />}

Boolean, null, undefined를 표시하려면 {String(value)} 식으로 작성한다.

PropTypes로 타입 확인하기

정적 타입을 사용하지 않는다면 사용할 만한 검증 라이브러리다. 원래는 React에 포함되어 있다가 분리된 모양이다. 개발 모드에서만 값을 검사한다. 자세한 사용법은 prop-types 참고한다.

class Greeting extends React.Component {
  // ...
}

Greeting.propTypes = {
  name: PropTypes.string,
  nicknames: PropTypes.arrayOf(PropTypes.string),
  children: PropTypes.element.isRequired
};

Greeting.defaultProps = {
  name: 'Stranger'
};

클래스 프로퍼티 문법으로도 사용할 수 있다.

정적 타입 검사

FlowTypeScript를 설정하고 사용하는 방법을 설명한다.

코틀린도 js를 타겟 플랫폼으로 사용 가능하다고 한다. Kotlin Wrappers, create-react-kotlin-app을 참고한다.

Refs와 DOM

일반적으로 데이터 흐름은 props를 사용하게 되어 있지만 몇몇 경우에는 이런 방식에 적합하지 않다.

  • 커서 위치, 텍스트 선택, 미디어 재생
  • 애니메이션 처리
  • 서드파티 DOM 라이브러리와 연동

선언적으로 해결할 수 있는 부분에서는 ref를 쓰지 않는 것을 권한다. 예를 들면 Dialog 컴포넌트에 open(), close() 메소드를 만드는 것보다 isOpen prop을 넘겨주는 식으로 처리한다. 안되는걸 되게 하려고 ref를 쓸 수는 있지만 쓰기 전에 컴포넌트 위계를 보고 상태를 어디에 위치해야 하는지 잘 고려해야 한다. ref를 쓰는 방식보다 상위 계층에 상태가 위치하는게 더 적절하다면 Lifting State Up 방식을 적용해서 해결한다.

Ref는 DOM 컴포넌트와 클래스 컴포넌트에서만 사용할 수 있다. 컴포넌트 자체를 레퍼런스로 넘기 때문인데 함수형 컴포넌트 내에서 DOM 컴포넌트나 클래스 컴포넌트에는 사용할 수 있다.

마운트 될 때는 인자에 해당 엘리먼트를 전달하고 언마운트에는 null을 전달한다. 이 refcomponentDidMount, componentDidUpdate 전에 호출된다.

일반적으로 DOM 엘리먼트에 접근해야 할 일이 있을 때 많이 쓴다. ref={input => this.textInput = input}

class AutoFocusTextInput extends React.Component {
  componentDidMount() {
    this.textInput.focusTextInput();
  }

  render() {
    return <CustomTextInput
      ref={(input) => { this.textInput = input; }}/>;
  }
}

하위 엘리먼트의 DOM ref를 상위에서 사용하려면 props 체인을 따라서 함수를 내려주면 된다. 여기서 inputRef는 일반 prop을 정의해서 쓴 것이지 ref처럼 특별한 기능이 있는 prop이 아니다.

function CustomTextInput(props) {
  return <div><input ref={props.inputRef} /></div>;
}

function FormLayout(props) {
  return (
    <div>
      Name: <CustomTextInput inputRef={props.inputRef} />
    </div>
  );
}

class AwesomePage extends React.Component {
  render() {
    return <FormLayout inputRef={el => this.inputElement = el} />;
  }
}

가능하면 DOM을 노출해서 사용하지 않는 것이 좋다고 한다. 어쩔 수 없이 필요할 때만 사용하고 극단적으로는 findDOMNode()라는 흑마법도 존재한다고.

ref가 두 차례씩 호출되는 것(마운트 && 언마운트)은 null을 전달해서 기존에 연결된 레퍼런스를 지우는 역할도 겸하고 있기 때문이다. (DOM 레퍼런스를 냅두면 DOM은 해제되어도 GC가 지우지 않고 남겨둔다. 그래서 복잡한거 하지 않도록 간단한 함수 형태로만 소개하는 것 같다.)

Uncontrolled 컴포넌트

폼 데이터를 React 컴포넌트에서 다루는 controlled 컴포넌트와 반대로 DOM 자체에서 다루도록 하는 방식의 컴포넌트를 뜻한다.

DOM 엘리먼트를 사용하면 내장된 동작을 그대로 사용할 수 있는 특징이 있다. Controlled and uncontrolled form inputs in React don’t have to be complicated 글에서 비교 도표를 볼 수 있다.

<input defaultValue="Bob" type="text" ref={(input) => this.input = input} />
// checkbox, radio는 defaultChecked, 그 외는 defaultValue

input[type="file"]은 읽기 전용으로 항상 uncontrolled 컴포넌트다. ref를 사용해서 DOM을 직접 다룬다.

성능 최적화

프로덕션 빌드를 사용한다.

  • Create React App (이래서 다 이거 얘기하는듯)
  • 단일 파일로 빌드
  • Brunch -p 옵션 빌드
  • Browserify의 envify, uglifyify, uglify-js
  • Rollup의 replace, commonjs, uglify
  • webpack

크롬 개발자 도구의 성능 탭에서 컴포넌트를 프로파일링한다. 프로파일링 전에 크롬 확장과 React DevTool 끄는 것 잊지 않는다. 성능 테스트 과정 참조.

긴 목록은 한번에 로드하지 말고 동적으로 처리해야 성능이 좋다. react virtualized 같은 패키지가 있고 Virtualize, windowing가 검색 키워드.

React devtool에서 Highlight updates 기능으로 불필요하게 렌더링이 되는 지점을 찾아서 수정한다.

shouldComponentUpdate()의 반환값으로 렌더링 여부를 수동으로 제어할 수 있다. 이 부분을 직접 작성하는 것보다 React.PureComponent를 상속받는게 낫다. PureComponent는 기존 Component 구현에 prop과 state의 shallow 비교를 포함하고 있다. shouldComponentUpdate() 메소드의 동작 방식과 구현은 본문을 읽는다. 갱신이 필요하면 노드를 타고 올라가서 모두 갱신하게 만든다.

불필요한 갱신은 가변 데이터에서 주로 나타나기 때문에 불변 데이터를 사용하면 이 문제를 쉽게 피할 수 있다. Array.push()로 기존 배열을 조작하는 것보다 Array.concat(), [...words, 'new data']을 사용해서 원 데이터가 변형되지 않도록 한다. Immutable.js을 써도 된다.

ES6 없이 React

ES6 없이 쓸 일이 있을 때 읽는다.

JSX 없이 React

JSX 없이 쓸 일이 있을 때 읽는다.


가이드 나머지 다 보고 나면 몇 가지 먼저 만들려고 한다. 그리고나서 enzyme이랑 상태 관리하는 패키지 redux랑 mobx? 찾아서 볼 생각이다. 많은 분들이 열심히 쓰고 있어서 주워들은 것만 공부해도 좀 걸릴 것 같다.

리액트 quick start 노트

😢 이 페이지는 다음 주소로 변경될 예정입니다.

리액트의 Quick start 페이지를 따라하면서 노트한 내용이다. js의 컨텍스트에서 이해할 수 있는 부분은 적지 않았다. 코드 스니핏도 간단히 알아볼 수 있게만 적어놔서 전체 내용이나 설명이 궁금하다면 본문을 확인하는게 좋겠다.

연습 환경 설치

node를 쓴지 오래되어서 업데이트부터 했다. nvm이 있어야 한다.

$ nvm ls-remote
$ nvm install v9.3.0 && nvm alias default v9.3.0
$ nvm use default
$ npm install -g yarn

npx를 이용해서 playground라는 이름으로 프로젝트를 생성한다. npx는 npm 5.2.0+부터 사용할 수 있다.

$ npx create-react-app playground
$ cd playground
$ yarn start

JSX

리액트는 마크업과 로직을 인위적으로 분리하지 않고 대신에 컴포넌트라는 단위를 만들어 약하게 결합하도록 만들었다. JSX를 꼭 사용할 필요는 없지만 시각적으로 더 편리하다.

리액트 엘리먼트는 ReactDOM.render()로 렌더링한다. root DOM 노드를 지정하면 그 내부의 모든 노드를 리액트가 관리한다.

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
function formatName(user) { /* ... * /}

const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
);

// 함수형 컴포넌트
function HelloBlock() {
  return <div>Hello</div>;
}

// 클래스 컴포넌트
class GoodbyeBlock extends React.Component {
  render() {
    return <div>Good bye</div>;
  }
}

노드 트리를 직접 조회하지 않고 ReactDOM을 통해 비교한 후, 변경된 사항만 반영하기 때문에 DOM을 직접 읽고 조작하는 방식보다 간결하다.

props

컴포넌트는 자바스크립트 함수와 같아서 인자 입력(props)을 받고 리액트 엘리먼트를 반환한다.

const Greeting = (props) => <h1>Hello, {props.name}!</h1>;

function Greeting(props) {
  return (
    <h1>Hello, {props.name}!</h1>
  );
}

class Greeting extends React.Component {
  render () {
    return <h1>Hello, {this.props.name}!</h1>
  }
}

props는 엘리먼트의 어트리뷰트로 전달한다. expression은 {}로, 문자열은 ""로 보낸다.

const element = <Comment user={user} text="string value blarblar" />;

모든 리액트 컴포넌트는 props를 변경하지 않는 순수 함수처럼 동작해야 한다. 이 규칙을 깨지 않고 출력값을 변경하기 위해 state라는 개념이 있다.

State

State는 props과 비슷하지만 private이고 컴포넌트가 전적으로 제어한다. state는 클래스로 작성한 컴포넌트에서 사용할 수 있다.

이 State를 제어하기 위해 componentDidMount()componentWillUnmount()와 같은 생애주기 훅(hook)을 사용한다. State에 대한 지정은 setState() 메소드를 사용한다.

this.state = {} 형태는 오직 constructor() 내에서만 사용 가능하며 그 외에는 setState()를 사용해야 한다. 그러지 않으면 렌더링에 반영되지 않는다.

한번 갱신하는데 setState() 호출을 여러 차례 한다면 경쟁 상태가 될 수 있다. 대신 함수 형태로 전달하는 것이 가능하다.

this.setState((prevState, props) => ({
  conter: prevState.counter + props.increment
}));

setState로 전달한 개체는 this.state에 병합되는 방식으로 동작한다. 전달하지 않은 프로퍼티는 영향을 받지 않는다.

state는 컴포넌트에 속했기 때문에 외부에서는 어떻게 정의되어 있는지 알 수 없고 알 필요도 없다. 지역적, 캡슐화되어 있다고 이야기하는 이유.

컴포넌트가 stateful, stateless인지는 때마다 다르게 정의해서 사용할 수 있음.

이벤트 제어

return <a href="#" onClick={handleClick}>Click Me</a>;

(이제는 시멘틱웹 얘기 부질 없는 것입니까. 나 너무 오래된 사람인듯.)

handleClicke.preventDefault()를 명시적으로 사용해야 함. (return false 넣는거 싫어하는 사람이라서 이런 방식 좋음.) 이 e는 W3C 스펙에서의 그 SyntheticEvent인데 리액트에 맞게 랩핑되어 있다.

이벤트에 넘겨줄 때는 js 특성 상 컨텍스트를 명시적으로 지정해야 한다. 즉, this 바인딩을 잊지 말아야 한다.

constructor(props) {
  super(props);
  // ...
  this.handleClick = this.handleClick.bind(this);
}

인자를 전달할 때는,

<button onClick={this.deleteRow.bind(this, id)}>Delete This</button>
// 이러면 클릭했을 때 `deleteRow(id, e)`로 호출해준다.

이 귀찮음을 피하기 위한 대안 두 가지로 public class fields 문법을 사용하는 방식과 익명 함수를 사용해 스코프를 전달하는 방식을 제안하는데 전자는 아직 확정된 문법이 아니다. 후자는 그나마 깔끔하지만 렌더링 될 때마다 새 콜백이 생성된다. 대부분 괜찮지만 하위 컴포넌트로 전달되었을 때 불필요한 추가 랜더링이 계속 나타나서 성능에 영향을 줄 수 있다.

// 1. public class fields syntax (experimental)
class Button extends React.Component {
  handleClick = () => {
    console.log('this is', this);
  }
  // ...
}

// 2. arrow funtion as callback
class Button extends React.Component {
  render () {
    return <button onClick={(e) => this.handleClick(e)}>Click me</button>;
  }
}

조건부 렌더링

if 사용하면 된다! 인라인으로 사용하고 싶다면 { expression && <p>Elements</p> } 식으로 사용한다. if-else는 삼항연산자를 사용한다.

null을 반환하면 화면에 렌더링하지 않는다. 예시로 에러 메시지 표시 나왔다. 렌더링 안해도 생애주기 훅은 여전히 호출된다.

리스트와 키

const faces = ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣'];
const listItems = faces.map((face, index) => (
  <li key={index}>
    {face}
  </li>
));

// 안정적인 id를 key로 사용하고 없으면 최후의 수단으로
// 배열 자체의 인덱스를 사용한다. 여기는 예시니까 그냥
// 인덱스를 사용했다.

const FaceList = () => <ul>{listItems}</ul>;

// or inline format
const FaceList = () => (
  <ul>
  {faces.map((face, index) => (
    <li key={index}>
      {face}
    </li>
  ))}
  </ul>
)

변화를 감지하고 반영하기 위해서는 id가 필요한데 key로 지정된 값을 활용한다. 또한 같은 계층에는 키가 유일해야 한다.

키는 리스트에서만 사용하고 각 항목 컴포넌트 정의에서 지정하지 않는다. 위에서 보면 li 역할하는 컴포넌트를 정의할 때 key를 지정하는게 아니라 listItems처럼 배열을 책임지는 컴포넌트에서 key를 지정해야 한다.

key는 prop처럼 작성하지만 실제로 해당 컴포넌트에 전달되진 않는다. 전달하려면 다른 이름의 prop을 명시적으로 지정해야 한다.

폼 Form

그냥 html 쓰듯 작성해도 문제 없다! 하지만 js가 있어야 미려한 기능을 만들 수 있는건 당연하고. 이럴 때는 controlled 컴포넌트를 만들어서 해결한다.

<input> 같은 엘리먼트는 상태를 스스로 관리한다. 리액트는 가변값을 state에 저장하고 setState()를 사용한다. 이 두가지를 하나로 합쳐 State를 “single source of truth”로 사용하고 사용자의 입력을 여기에 반영하는 식으로 만든다. 폼 엘리먼트를 리액트에서 관리하니까 controlled 컴포넌트라고 한다.

// bind 생략
handleChange(event) {
  this.setState({
    address: event.target.value
  });

  // 여러 input을 처리할 때는 computed property name 문법으로
  this.setState({
    [event.target.name]: event.target.value
  });
}
// ...
render() {
  return <input name="address" type="text" value={this.state.value} onChange={this.handleChange} />
}

input[type="file"]은 읽기 전용인데 이런 경우는 uncontrolled 컴포넌트라고 한다.

value에 직접 값을 전달하면 사용자가 값을 변경할 수 없다. 값을 전달한 이후에 다시 값을 변경할 수 있게 하려면 undefinednull을 다시 전달해야 한다.

state 위로 보내기 lifting state up

prop에 state를 조작할 수 있는 함수를 만들어서 전달하면 자식 노드에서도 부모 노드의 state를 간접적으로 조작할 수 있다. 이 방법을 “lifting state up” 라고 말한다.

아래는 NameInput에서 변경된 내용을 WelcomeBoard에서 전달받은 onNameChange를 사용해 갱신하는 방식이다.

class NameInput extends React.Component {
  handleChange(e) {
    this.props.onNameChange(e.target.value);
  }
  render() {
    return (
      <input type="text"
        value={this.props.name}
        onChange={this.handleChange.bind(this)} />
    )
  }
}

class WelcomeBoard extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: 'stranger'};
  }

  handleNameChange(name) {
    this.setState({
      name: name === '' ? 'stranger' : name,
    });
  }

  render() {
    return (
      <div>
        <p>Hello, {this.state.name}!</p>
        <NameInput
          name={this.state.name}
          onNameChange={this.handleNameChange.bind(this)} />
      </div>
    )
  }
}

구성 vs 상속

자식 노드로 어떤 것이 오게 될지 미리 알 수 없다. 그래서 특별한 prop으로 children이 존재한다.

function Box() {
  return <div>{props.children}</div>;
}

이제 다음과 같이 사용하면 알아서 내부 노드로 처리한다.

<Box>
  <NameInput />
  <NameInput />
  <NameInput />
</Box>

Containment의 예시로 SplitPane를 작성했다.

Specialization의 예시로 Dialog를 작성했다.

상속을 사용하는 경우는 적절한 유즈케이스가 없다고 한다. UI와 관련되지 않은 로직을 공유해야 할 경우라도 상속보다는 별도의 JS 모듈로 작성해서 import 하는 방식을 권장한다.

리액트 식으로 생각하기

리액트를 사용할 때 어떤 방식으로 접근해야 하는지 설명한다.

  1. Mock에서 시작. 프로토타이핑과 mock 데이터를 갖고 시작한다.
  2. UI를 나눠 컴포넌트 계층을 만든다.
    • 단일 책임 원칙
    • 데이터 구조에 맞게
  3. React에서 정적인 페이지로 작성한다.
    • state를 사용하지 않음
    • 데이터는 단방향으로
  4. UI state의 Representation을 모든 경우를 소화할 수 있으면서도 최소한으로 파악한다.
  5. state가 어디에 위치해야 하는지 파악한다.
    • owner 컴포넌트, 또는 상위 컴포넌트
    • 적당한 컴포넌트가 없다면 빈 컴포넌트라도 만들어서
  6. 데이터 흐름이 반대가 되야 하는 경우를 파악한다.
    • lifting state up