서론

리액트를 사용하신다면, Immutability 라는 말, 한번쯤은 들어보셨을겁니다. 리액트 컴포넌트의 state 를 변경해야 할 땐, 무조건, setState 를 통해서 업데이트 해주어야 하며, 업데이트 하는 과정에서 기존의 객체의 값을 직접적으로 수정하면 절대!! 안됩니다.

예를 들어서 컴포넌트의 state 에 users 라는 배열이 있다고 가정해봅시다.

state = {
  users: [
    {
      id: 1,
      username: 'velopert'
    }
  ]
};

자, 우리가 만약에 이 users 배열에 새로운 객체를 추가한다면 어떻게 해야 할까요? 기존에 jQuery 를 사용하여 웹개발을 하셨거나, Angular 의 양방향 바인딩에 익숙하다면 다음과 같은 코드를 작성하고 싶을지도 모릅니다.

// bad!
this.state.users.push({ 
  id: 2, 
  username: 'mjkim' 
});

만약에 username 을 변경하고 싶다면? 이렇게 하고 싶을 수도 있겠죠.

// bad!!
this.state.users[0].username = 'new_velopert';

하지만, 이렇게 하시면 절대로 안됩니다. 위 처럼 코드를 작성하시면 정말 큰일나요!

우선, setState 를 통하여 state 를 변경하지 않으면 리렌더링이 되지 않습니다. 그렇다고 해서.. 가끔씩 이렇게 하시는 분들도 있는데요:

// bad!!!
this.state.users.push({ 
  id: 2, 
  username: 'mjkim' 
});

this.setState({
  users: this.state.users
});
// bad!!!!
this.setState(({users}) => {
  users.push({ 
    id: 2, 
    username: 'mjkim' 
  });
  return { 
    users
  };
});

이것도 절대로!! 안됩니다. setState 를 통해서 하니까 컴포넌트가 리렌더링은 되겠지만요, 나중에 컴포넌트 최적화를 못하게 됩니다.

불변함을 유지 않으면 왜 컴포넌트 최적화가 안돼?

Edit 컴포넌트 최적화하기

자, 위 링크를 열어서 온라인 IDE 를 실행하세요. 다음과 같은 화면이 나옵니다.

콘솔쪽을 보시면, 컴포넌트가 렌더링 될 때마다 기록이 되고 있습니다.

이 포스트에서는 Codesandbox 를 사용해서 튜토리얼을 진행합니다. 원한다면, create-react-app 으로 프로젝트를 생성해서 src 디렉토리 내부에 해당 파일들을 생성하여 작업을 진행하셔도 좋습니다.ㅍ

코드를 한번 쭉 훑어볼까요?

User.js

import React, { Component } from 'react';

class User extends Component {
  render() {
    const { user: { username } } = this.props;
    console.log('%s가 렌더링 되고있어요!!!', username);

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

export default User;

UserList.js

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

class UserList extends Component {

  renderUsers = () => {
    const { users } = this.props;
    return users.map((user) => (
      <User key={user.id} user={user}/>
    ))
  }

  render() {
    console.log('UserList 가 렌더링되고 있어요!')
    const { renderUsers } = this;
    return (
      <div>
        {renderUsers()}
      </div>
    );
  }
}

export default UserList;

App.js

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

class App extends Component {
  id = 3;

  state = {
    input: '',
    users: [
      {
        id: 1,
        username: 'velopert'
      },
      {
        id: 2,
        username: 'mjkim'
      }
    ]
  }

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

  onButtonClick = (e) => {
    this.setState(({ users, input }) => ({
      input: '',
      users: users.concat({
        id: this.id++,
        username: input
      })
    }))
  }

  render() {
    const { onChange, onButtonClick } = this;
    const { input, users } = this.state;

    return (
      <div>
        <div>
          <input onChange={onChange} value={input} />
          <button onClick={onButtonClick}>추가</button>
        </div>
        <h1>사용자 목록</h1>
        <div>
          <UserList users={users} />
        </div>
      </div>
    );
  }
}

export default App;

자, 화면에 보여지는 인풋에 입력을 해보세요 (추가버튼은 누르지말고 텍스트만 입력해보세요)

abcd 라고 입력을 해봤습니다. 배열이 바뀔때마다 렌더 함수가 실행 되고있죠?

이것은 리액트의 기본적인 속성입니다. 부모 컴포넌트가 리렌더링 되면, 자식 컴포넌트들 또한 리렌더링이 됩니다. 이 과정은, 가상 DOM 에만 이뤄지는 렌더링이며, 렌더링을 마치고, 리액트의 diffing 알고리즘을 통하여 변화가 일어나는 부분만 실제로 업데이트 해줍니다.

지금은 인풋 내용이 수정 될 때마다 UserList 도 새로 렌더링이 되고있습니다. 아무리 실제 DOM 에는 반영되지는 않겠지만, 그래도 CPU 쪽에 미세한 낭비가 발생하게 되죠.

지금의 규모의 프로젝트에서는, 이런건 전혀 문제가 되지 않습니다. 하지만, 여러분들이 규모가 큰 프로젝트를 작업하시게 된다면, 저런게 쌓이고 쌓여서 어쩌면 서비스에 버벅임이 발생하게 될 수도 있습니다.

우리는 코드상에서 불변함을 유지하면서 코드를 작성했기에 이 부분을 아주 쉽게 최적화를 할 수 있습니다. 한줄짜리 shouldComponentUpdate 를 구현해주면 되죠.

UserList.js

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

class UserList extends Component {

  shouldComponentUpdate(prevProps, prevState) {
    return prevProps.users !== this.props.users;
  }


  renderUsers = () => {
    const { users } = this.props;
    return users.map((user) => (
      <User key={user.id} user={user} />
    ))
  }

  render() {
    console.log('UserList 가 렌더링되고 있어요!')
    const { renderUsers } = this;
    return (
      <div>
        {renderUsers()}
      </div>
    );
  }
}

export default UserList;

User.js

import React, { Component } from 'react';

class User extends Component {
  shouldComponentUpdate(prevProps, prevState) {
    return this.props.user !== prevProps.user;
  }
  render() {
    const { user: { username } } = this.props;
    console.log('%s가 렌더링 되고있어요!!!', username);

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

export default User;

최적화된 프로젝트 확인하기

Edit 컴포넌트 최적화하기

이러한 이유 때문에 우리는 state 를 업데이트 할 때는 불변함을 유지하면서 업데이트 해주어야 합니다.

불변함을 유지하다보면 코드가 좀 복잡해진다

추후 진행할 최적화를 위하여 불변함을 유지하며 코드를 작성하다보면 가끔씩 복잡해질 때가 있습니다. 예를 들어 다음과 같은 상태가 있다고 가정해봅시다.

state = {
  users: [
    { 
      id: 1, 
      username: 'velopert', 
      email: 'public.velopert@gmail.com' 
    },
    { 
      id: 2, 
      username: 'lopert', 
      email: 'lopert@gmail.com' 
    }
  ]
}

이렇게 두가지의 객체가 배열 안에 있을 때, 두번째 계정의 이메일을 변경하고 싶다면 이렇게 해야합니다.

const { users } = this.state;
const nextUsers = [ ...users ]; // users 배열을 복사하고
nextUsers[1] = {
  ...users[index], // 기존의 객체 내용을 복사하고
  email: 'new_lopert@gmail.com' // 덮어 씌우고
};
// 이렇게 기존의 users 는 건들이지 않고
// 새로운 배열/객체를 만들어 setState
this.setState({
  users: nextUsers
});

혹은, 수정하고 싶은 state 가 어쩌다가 보니 아주 깊은 구조로 되어있다면 어떨까요?

state = {
  where: {
    are: {
      you: {
        now: 'faded',
        away: true // 요놈을 바꾸고 싶다!
      },
      so: 'lost'
    },
    under: {
      the: true,
      sea: false
    }
  }
}

위 state 를 불변함을 유지하면서 업데이트 하려면 완전 귀찮습니다.

const { where } = this.state;
this.setState({
  where: {
    ...where,
    are: {
      ...where.are,
      you: {
        ...where.are.you,
        away: false
      }
    }
  }
});

이렇게 해야 비로소! 기존의 객체는 건들이지 않고 새 객체를 생성하여 불변함을 유지하며 값을 업데이트 할 수 있습니다.

애초에 state 의 구조를 저렇게 복잡하게 하면 안되긴 하지만, 위와 같은 작업을 매번 하기는 엄청나게 번거롭습니다. 실수 할 수도 있구요.

이러한 작업을 쉽게 해줄 수 있는 것이 바로 Immutable.js 입니다!

results matching ""

    No results matching ""