React Advanced

 

Optimizing Performance

Immutable.js

immer

createPortal

Lead Software Engineer @ProtoPie

Microsoft MVP

TypeScript Korea User Group Organizer

Marktube (Youtube)

Mark Lee

이 웅재

Optimizing Performance

필요할 때만 랜더한다.

Reconciliation

  • 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.

  • 개발자가 key prop 을 통해,
    여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.

class App extends React.Component {
  state = {
    count: 0
  };
  componentDidMount() {
    setInterval(() => {
      this.setState({
        count: this.state.count + 1
      });
    }, 1000);
  }
  render() {
    if (this.state.count % 2 === 0) {
      return (
        <div>
          <Foo />
        </div>
      );
    }
    return (
      <span>
        <Foo />
      </span>
    );
  }
}
class Foo extends React.Component {
  componentDidMount() {
    console.log("Foo componentDidMount");
  }

  componentWillUnmount() {
    console.log("Foo componentWillUnmount");
  }

  render() {
    return <p>Foo</p>;
  }
}

엘리먼트의 타입이 다른 경우

class App extends React.Component {
  state = {
    count: 0
  };
  componentDidMount() {
    setInterval(() => {
      this.setState({
        count: this.state.count + 1
      });
    }, 1000);
  }
  render() {
    if (this.state.count % 2 === 0) {
      return <div className="before" title="stuff" />;
    }
    return <div className="after" title="stuff" />;
  }
}

DOM 엘리먼트의 타입이 같은 경우 (1)

class App extends React.Component {
  state = {
    count: 0
  };
  componentDidMount() {
    setInterval(() => {
      this.setState({
        count: this.state.count + 1
      });
    }, 1000);
  }
  render() {
    if (this.state.count % 2 === 0) {
      return <div style={{ color: "red", fontWeight: "bold" }} />;
    }
    return <div style={{ color: "green", fontWeight: "bold" }} />;
  }
}

DOM 엘리먼트의 타입이 같은 경우 (2)

class App extends React.Component {
  state = {
    count: 0
  };
  componentDidMount() {
    setInterval(() => {
      this.setState({
        count: this.state.count + 1
      });
    }, 1000);
  }
  render() {
    if (this.state.count % 2 === 0) {
      return <Foo name="Mark" />;
    }
    return <Foo name="Anna" />;
  }
}

같은 타입의 컴포넌트 엘리먼트

class Foo extends React.Component {
  state = {};

  componentDidMount() {
    console.log("Foo componentDidMount");
  }

  componentWillUnmount() {
    console.log("Foo componentWillUnmount");
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log("Foo getDerivedStateFromProps", nextProps, prevState);
    return {};
  }

  render() {
    console.log("Foo render");
    return <p>Foo</p>;
  }
}
class App extends React.Component {
  state = {
    count: 0
  };
  componentDidMount() {
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1
      });
    }, 3000);
  }
  render() {
    if (this.state.count % 2 === 0) {
      return (
        <ul>
          <Foo>first</Foo>
          <Foo>second</Foo>
        </ul>
      );
    }
    return (
      <ul>
        <Foo>first</Foo>
        <Foo>second</Foo>
        <Foo>third</Foo>
      </ul>
    );
  }
}

자식에 대한 재귀적 처리 (1)

class Foo extends React.Component {
  state = {};

  componentDidMount() {
    console.log("Foo componentDidMount", this.props.children);
  }

  componentWillUnmount() {
    console.log("Foo componentWillUnmount");
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log("Foo getDerivedStateFromProps", nextProps, prevState);
    return {};
  }

  render() {
    console.log("Foo render", this.props.children);
    return <p>{this.props.children}</p>;
  }
}
class App extends React.Component {
  state = {
    count: 0
  };
  componentDidMount() {
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1
      });
    }, 3000);
  }
  render() {
    if (this.state.count % 2 === 0) {
      return (
        <ul>
          <Foo>second</Foo>
          <Foo>third</Foo>
        </ul>
      );
    }
    return (
      <ul>
        <Foo>first</Foo>
        <Foo>second</Foo>
        <Foo>third</Foo>
      </ul>
    );
  }
}

자식에 대한 재귀적 처리 (2)

class Foo extends React.Component {
  state = {};

  componentDidMount() {
    console.log("Foo componentDidMount", this.props.children);
  }

  componentWillUnmount() {
    console.log("Foo componentWillUnmount");
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log("Foo getDerivedStateFromProps", nextProps, prevState);
    return {};
  }

  render() {
    console.log("Foo render", this.props.children);
    return <p>{this.props.children}</p>;
  }
}
class App extends React.Component {
  state = {
    count: 0
  };
  componentDidMount() {
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1
      });
    }, 3000);
  }
  render() {
    if (this.state.count % 2 === 0) {
      return (
        <ul>
          <Foo key="2">second</Foo>
          <Foo key="3">third</Foo>
        </ul>
      );
    }
    return (
      <ul>
        <Foo key="1">first</Foo>
        <Foo key="2">second</Foo>
        <Foo key="3">third</Foo>
      </ul>
    );
  }
}

자식에 대한 재귀적 처리 (3)

class Foo extends React.Component {
  state = {};

  componentDidMount() {
    console.log("Foo componentDidMount", this.props.children);
  }

  componentWillUnmount() {
    console.log("Foo componentWillUnmount");
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log("Foo getDerivedStateFromProps", nextProps, prevState);
    return {};
  }

  render() {
    console.log("Foo render", this.props.children);
    return <p>{this.props.children}</p>;
  }
}
class App extends React.Component {
  state = {
    text: "",
    persons: [
      { id: 1, name: "Mark", age: 37 },
      { id: 2, name: "Anna", age: 26 },
    ]
  };
  render() {
    console.log("App render");
    const { text, persons } = this.state;
    return (
      <div>
        <input type="text" value={text} onChange={this._change} />
        <button onClick={this._click}>click</button>
        <ul>
          {persons.map(p => (
            <Person {...p} key={p.id} />
          ))}
        </ul>
      </div>
    );
  }

  _change = e => {
    this.setState({
      ...this.state,
      text: e.target.value
    });
  };

  _click = () => {
    console.log(this.state.text);
  };
}

setState 와 함께 일어나는 일

class Person extends React.Component {
  render() {
    console.log("Person render");

    const { name, age } = this.props;
    return (
      <ul>
        {name} / {age}
      </ul>
    );
  }
}
class App extends React.Component {
  state = {
    text: "",
    persons: [
      { id: 1, name: "Mark", age: 37 },
      { id: 2, name: "Anna", age: 26 },
    ]
  };
  render() {
    console.log("App render");
    const { text, persons } = this.state;
    return (
      <div>
        <input type="text" value={text} onChange={this._change} />
        <button onClick={this._click}>click</button>
        <ul>
          {persons.map(p => (
            <Person {...p} key={p.id} />
          ))}
        </ul>
      </div>
    );
  }

  _change = e => {
    this.setState({
      ...this.state,
      text: e.target.value
    });
  };

  _click = () => {
    console.log(this.state.text);
  };
}

shouldComponentUpdate

class Person extends React.Component {
  shouldComponentUpdate(previousProps) {
    for (const key in this.props) {
      if (previousProps[key] !== this.props[key]) {
        return true;
      }
    }
    return false;
  }
  render() {
    console.log("Person render");

    const { name, age } = this.props;
    return (
      <ul>
        {name} / {age}
      </ul>
    );
  }
}
class App extends React.Component {
  state = {
    text: "",
    persons: [
      { id: 1, name: "Mark", age: 37 },
      { id: 2, name: "Anna", age: 26 },
    ]
  };
  render() {
    console.log("App render");
    const { text, persons } = this.state;
    return (
      <div>
        <input type="text" value={text} onChange={this._change} />
        <button onClick={this._click}>click</button>
        <ul>
          {persons.map(p => (
            <Person {...p} key={p.id} />
          ))}
        </ul>
      </div>
    );
  }

  _change = e => {
    this.setState({
      ...this.state,
      text: e.target.value
    });
  };

  _click = () => {
    console.log(this.state.text);
  };
}

PureComponent

class Person extends React.PureComponent {
  render() {
    console.log("Person render");

    const { name, age } = this.props;
    return (
      <ul>
        {name} / {age}
      </ul>
    );
  }
}
class App extends React.Component {
  state = {
    text: "",
    persons: [
      { id: 1, name: "Mark", age: 37 },
      { id: 2, name: "Anna", age: 26 },
    ]
  };
  render() {
    console.log("App render");
    const { text, persons } = this.state;
    return (
      <div>
        <input type="text" value={text} onChange={this._change} />
        <button onClick={this._click}>click</button>
        <ul>
          {persons.map(p => (
            <Person {...p} key={p.id} onClick={() => {}} />
          ))}
        </ul>
      </div>
    );
  }

  _change = e => {
    this.setState({
      ...this.state,
      text: e.target.value
    });
  };

  _click = () => {
    console.log(this.state.text);
  };
}

onClick={() => {}}

class Person extends React.PureComponent {
  render() {
    console.log("Person render");

    const { name, age } = this.props;
    return (
      <ul>
        {name} / {age}
      </ul>
    );
  }
}
class App extends React.Component {
  state = {
    text: "",
    persons: [
      { id: 1, name: "Mark", age: 37 },
      { id: 2, name: "Anna", age: 26 },
    ]
  };
  render() {
    console.log("App render");
    const { text, persons } = this.state;
    return (
      <div>
        <input type="text" value={text} onChange={this._change} />
        <button onClick={this._click}>click</button>
        <ul>
          {persons.map(p => (
            <Person {...p} key={p.id} onClick={() => {}} />
          ))}
        </ul>
      </div>
    );
  }

  _change = e => {
    this.setState({
      ...this.state,
      text: e.target.value
    });
  };

  _click = () => {
    console.log(this.state.text);
  };
}

React.memo

const Person = React.memo(props => {
  console.log("Person render");

  const { name, age } = props;
  return (
    <ul>
      {name} / {age}
    </ul>
  );
});

createPortal

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    ...
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <div id="modal"></div>
  </body>
</html>

src/index.css

body {...}

#modal {
  position: absolute;
  top: 0;
  left: 0;
}

src/components/Modal.jsx

import ReactDOM from 'react-dom';

const Modal = ({ children }) =>
  ReactDOM.createPortal(children, document.querySelector('#modal'));

export default Modal;

src/pages/Home.jsx

const Home = () => {
  const [visible, setVisible] = useState(false);
  const show = () => setVisible(true);
  const hide = () => setVisible(false);
  return (
    <div>
      <h1>Home</h1>
      <button onClick={click}>open</button>

      <NaviContainer />
      <BooksContainer />

      {visible && (
        <Modal>
          <div
            style={{
              width: '100vw',
              height: '100vh',
              background: 'rgba(0, 0, 0, 0.5)',
            }}
            onClick={hide}
          >
            Hello
          </div>
        </Modal>
      )}
    </div>
  );
};
npm i immutable

immutable-js 주요 특징

  • 한번 만들어지면 변하지 않는 데이터를 "이뮤터블 데이터" 라 한다.
  • 자바스크립트는 객체 내부의 값을 변경해도 "같은 레퍼런스" 를 유지하기 때문에,
    객체 내부 값을 일일이 비교하지 않으면 정확히 모든 값이 같은지 알수 없다.
  • A 라는 "이뮤터블 데이터" 에서 특정 값을 수정하려면,
    기본적으로는 B 라는 객체를 새로 만들고 A 의 모든 값을 B 로 복사해야한다.
    • 이 것은 비용이 많이 든다.
    • 그래서 나온 아이디어로 구현해낸 것이 immutable-js
  • react 의 어느 부분에서 사용하면 좋을까
    • 첫번째, 리덕스의 리듀서 부분
    • 두번째, Pure 컴포넌트

기본 사용법 (1) - Map

import { Map } from "immutable";

const object = Map({ a: 1, b: 2, c: 3 });
console.log(object);

const nested = Map({ foo: Map({ name: "Mark", age: 37 }), bar: "bar" });
console.log(nested);

기본 사용법 (2) - List

import { List, Map } from "immutable";

const list1 = List([1, 2, 3]);
console.log(list1);
console.log(list1.toJS());

const list2 = List([
  Map({ name: "Mark", age: 37 }),
  Map({ name: "Anna", age: 26 })
]);
console.log(list2);
console.log(list2.toJS());

const list3 = list1.push(4, 5);
const list4 = list3.unshift(0);
const list5 = list4.concat(list2, list3);
console.log(list1.size);
console.log(list3.size);
console.log(list4.size);
console.log(list5.size);

기본 사용법 (2) - List

기본 사용법 (3) - set, get

import { List, Map } from "immutable";

const object = Map({ a: 1, b: 2, c: 3 });
const list = List([1, 2, 3]);

const newObject = object.set("a", 5);
console.log(object === newObject, newObject.get("a"));

const newList = list.set(0, 5);
console.log(list === newList, newList.get(0));

기본 사용법 (4) - setIn, getIn

import { List, Map } from "immutable";

const object = Map({ foo: Map({ name: "Mark", age: 37 }), bar: "bar" });
const list = List([
  Map({ name: "Mark", age: 37 }),
  Map({ name: "Anna", age: 26 })
]);

const newObject = object.setIn(["foo", "name"], "Anna");
console.log(object === newObject, newObject.getIn(["foo", "name"]));

const newList = list.setIn([0, "name"], "Anna");
console.log(list === newList, newList.getIn([0, "name"]));

기본 사용법 (5) - update, updateIn

import { Map } from "immutable";

const object = Map({ a: 1, b: 2, c: 3 });
const nested = Map({ foo: Map({ name: "Mark", age: 37 }), bar: "bar" });

const newObject = object.update("a", value => value + 1);
console.log(object === newObject, newObject.get("a"));

const newNested = nested.updateIn(["foo", "age"], value => value + 1);
console.log(object === newNested, newNested.getIn(["foo", "age"]));

기본 사용법 (6) - delete

import { Map } from "immutable";

const object = Map({ a: 1, b: 2, c: 3 });

const newObject = object.delete("c");
console.log(object === newObject, newObject.toJS(), newObject.get("c"));

before Using immutable-js

// src/reducers/books.js

export default function books(state = initialState, action) {
  switch (action.type) {
    case RECEIVE_BOOKS:
      return [...state, ...action.books];

    case DELETE_BOOK: {
      const newState = [...state];
      for (let i = 0; i < newState.length; i++) {
        if (newState[i].bookId === action.bookId)
          newState[i].deletedAt = new Date().toISOString();
      }
      return newState;
    }

    case UNDO_DELETE_BOOK: {
      const newState = [...state];
      for (let i = 0; i < newState.length; i++) {
        if (newState[i].bookId === action.bookId) newState[i].deletedAt = null;
      }
      return newState;
    }

    default:
      return state;
  }
}

Use immutable-js

// src/reducers/books.js

const initialState = List([]);

export default function books(state = initialState, action) {
  switch (action.type) {
    case RECEIVE_BOOKS: {
      return state.concat(action.books.map(book => Map(book)));
    }
    case DELETE_BOOK: {
      return state.map(book => {
          if (book.get(bookId) === action.bookId)
            book.set(deletedAt, new Date().toISOString());
          return book;
        });
    }

    case UNDO_DELETE_BOOK: {
      return state.map(book => {
          if (book.get(bookId) === action.bookId) book.set(deletedAt, null);
          return book;
        });
    }

    default:
      return state;
  }
}
npm i immer

immer 주요 특징

  • produce(객체, 함수) 를 이용한 draft 처리
  • produce(함수) 를 이용한 setState(함수) 처리

기본 사용법 - produce (객체, 함수)

import produce from 'immer';

const state = {
  name: 'Mark',
  age: 39,
};

const nextState = produce(state, (draft) => {
  draft.age += 1;
});

console.log(state, nextState, state === nextState);

기본 사용법 - produce(객체, 함수)

import produce from 'immer';

const state = {
  books: [
    { title: '책 이름', author: { name: 'Mark', age: 39 } },
    { title: '책 이름', author: { name: 'Hanna', age: 28 } },
  ],
};

const nextState = produce(state, (draft) => {
  draft.books[0].author.age += 1;
});

console.log(state, nextState, state === nextState);
console.log(state.books[1] === nextState.books[1]);

기본 사용법 - produce(함수)

import produce from 'immer';

const state = {
  books: [
    { title: '책 이름', author: { name: 'Mark', age: 39 } },
    { title: '책 이름', author: { name: 'Hanna', age: 28 } },
  ],
};

console.log(
  produce((draft) => {
    draft.books[0].author.age += 1;
  }),
);

기본 사용법 - produce(함수)

function App() {
  const [state, setState] = useState({
    books: [
      { title: '책 이름', author: { name: 'Mark', age: 39 } },
      { title: '책 이름', author: { name: 'Hanna', age: 28 } },
    ],
  });

  useEffect(() => {
    setState(
      produce((draft) => {
        draft.books[0].author.age += 1;
      }),
    );
  }, []);

  console.log(state);

  return (
    <div>{JSON.stringify(state)}</div>
  );
}