React ❤️ Immutable.js – 리액트의 불변함, 그리고 컴포넌트에서 Immutable.js 사용하기


이 포스트는 React 에서는 불변함 (Immutability) 를 지키며 상태 관리를 하는 것을 매우 편하게 해주는 라이브러리 Immutable.js 에 대해서 알아보겠습니다.

서론

리액트를 사용하신다면, 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(nextProps, nextState) {
    return nextProps.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(nextProps, nextState) {
    return this.props.user !== nextProps.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 입니다!

Immutable.js 시작하기

프로젝트에서 immutable 을 사용 할 땐, 다음과 같이 패키지를 설치해서 사용합니다.

yarn add immutable

그런데 우리는 이 튜토리얼에서는 Codesandbox 를 사용하므로, 좌측의 Dependencies 를 누른다음에 Add Package 를 누르시면 패키지를 직접 설치하여 사용 할 수 있습니다. 여러모로 유용한 도구입니다.

Immutable 을 사용 할 때는 다음 규칙들을 기억하세요:

  1. 객체는 Map
  2. 배열은 List
  3. 설정할땐 set
  4. 읽을땐 get
  5. 읽은다음에 설정 할 땐 update
  6. 내부에 있는걸 ~ 할땐 뒤에 In 을 붙인다: setIn, getIn, updateIn
  7. 일반 자바스크립트 객체로 변환 할 땐 toJS
  8. List 엔 배열 내장함수와 비슷한 함수들이 있다 – push, slice, filter, sort, concat… 전부 불변함을 유지함
  9. 특정 key 를 지울때 (혹은 List 에서 원소를 지울 때) delete 사용

프로젝트의 엔트리인 index.js 에서 시험삼아 Immutable 을 불러오고, Map 과 List 를 사용해보세요.

다음 코드를 하나하나 이해하면서 직접 작성해보시길 바랍니다.

index.js

import React from 'react';
import { render } from 'react-dom';
import App from './App';
import { Map, List } from 'immutable';

// 1. 객체는 Map
const obj = Map({
  foo: 1,
  inner: Map({
    bar: 10
  })
});

console.log(obj.toJS());

// 2. 배열은 List
const arr = List([
  Map({ foo: 1 }),
  Map({ bar: 2 }),
]);

console.log(arr.toJS());

// 3. 설정할땐 set
let nextObj = obj.set('foo', 5);
console.log(nextObj.toJS());
console.log(nextObj !== obj); // true

// 4. 값을 읽을땐 get
console.log(obj.get('foo'));
console.log(arr.get(0)); // List 에는 index 를 설정하여 읽음

// 5. 읽은다음에 설정 할 때는 update
// 두번째 파라미터로는 updater 함수가 들어감 
nextObj = nextObj.update('foo', value => value + 1);
console.log(nextObj.toJS());

// 6. 내부에 있는걸 ~ 할땐 In 을 붙인다
nextObj = obj.setIn(['inner', 'bar'], 20);
console.log(nextObj.getIn(['inner', 'bar']));

let nextArr = arr.setIn([0, 'foo'], 10);
console.log(nextArr.getIn([0, 'foo']));

// 8. List 내장함수는 배열이랑 비슷하다
nextArr = arr.push(Map({ qaz: 3 }));
console.log(nextArr.toJS());
nextArr = arr.filter(item => item.get('foo') === 1);
console.log(nextArr.toJS());

// 9. delete 로 key 를 지울 수 있음
nextObj = nextObj.delete('foo');
console.log(nextObj.toJS());

nextArr = nextArr.delete(0);
console.log(nextArr.toJS());


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

갑자기 이런걸 사용하게 되면 처음엔 적응이 안될지도 모릅니다. 한번 사용해보고 모두 외우는건 무리일수도 있습니다. 일단 이런것들이 있다는것만 알아두고, 앞으로 Immutable 을 사용하면서 차근차근 적응해나가면 되니까 큰 걱정은 하지 마세요~

사실 이 포스트에서 나온건 Immutable 의 극히 일부분입니다. 매뉴얼 을 보시면 더 많은 기능을 확인 하실 수 있습니다. 하지만, 주로 사용 되는건 위 9가지 입니다.

다 확인해보셨다면, 기존에 index.js 에 작성한것들은 지우세요.

리액트 컴포넌트에서 Immutable 사용하기

여기서부터 본격적으로 리액트 컴포넌트에 Immutable 를 사용하는 것을 배워보겠습니다. 우선적으로, Immutable 은 페이스북에서 만들었기 때문에 React 와 호환이 어느정도 되긴 합니다 (예를 들어서 JSX 이뤄진 List 를 렌더링 할 수 있습니다) 하지만, state 자체를 Immutable 데이터로 사용하는것 까지는 지원되지 않습니다.

따라서, state 내부에 하나의 Immutable 객체를 만들어두고, 상태 관리를 모두 이 객체를 통해서 진행하시면 됩니다.

우선 state 부터 변경해보세요:

  state = {
    data: Map({
      input: '',
      users: List([
        Map({
          id: 1,
          username: 'velopert'
        }),
        Map({
          id: 2,
          username: 'mjkim'
        })
      ])
    })
  }

data 라는 Map 을 만들었고, 그 내부에는 users List 가 있고, 그 안에 또 Map 두개가 안에 들어있습니다.

이렇게 수정을 하고 나면 코드에서 오류가 나기 시작 할 것입니다. 우리가 함께 차차 고쳐줄테니 당황하지 마세요.

이제 setState 를 하게 될 때도 코드를 조금씩 바꿔줘야 합니다.

onChange 부분도 다음과 같이 수정하세요.

  onChange = (e) => {
    const { value } = e.target;
    const { data } = this.state;

    this.setState({
      data: data.set('input', value)
    });
  }

그렇게 복잡하진 않죠? set 을 통해서 값을 변경해줬습니다.

그 다음엔, onButtonClick 도 수정해보세요.

  onButtonClick = () => {
    const { data } = this.state;

    this.setState({
      data: data.set('input', '')
        .update('users', users => users.push(Map({
          id: this.id++,
          username: data.get('input')
        })))
    })
  }

여기는 이전 코드보다 아주 조금은 복잡해졌습니다. 그래도, 해석하지 못할 정도는 아닙니다. 이 함수에서는 input 값을 공백으로 만들어야 하고, users 에 새 Map 을 추가해주어야 합니다. 이렇게 여러가지를 하는 경우에는 함수들을 중첩하여 사용하면 됩니다. data.set(…).update(…) 형식으로 말이죠.

상태 업데이트 로직이 완성되고 나면, render 함수도 변경해주어야 합니다. Map 혹은 List 의 값을 읽을땐 data.users 이런식으로는 읽지 못하고, data.get(‘users’) 이런식으로 읽어야 합니다.

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

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

UserList 와 User 에서도 마찬가지로 값을 읽어올 때 get 을 사용해주어야 합니다.

UserList.js

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

class UserList extends Component {

  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.users !== this.props.users;
  }


  renderUsers = () => {
    const { users } = this.props;
    return users.map((user) => (
      <User key={user.get('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(nextProps, nextState) {
    return this.props.user !== nextProps.user;
  }
  render() {
    const { username } = this.props.user.toJS();
    console.log('%s가 렌더링 되고있어요!!!', username);

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

export default User;

User 컴포넌트에서는, username 을 보여주기 위해서, const username = this.props.user.get('username') 을 해도 좋습니다. 하지만 위와 같은 형식으로 toJS() 를 한 결과를 비구조화 할당 하는 방법도 있답니다.

여기까지 하시면, Immutable 를 사용하여 상태관리하는것이 완성됩니다.

Edit 시작하세요 Immutable.js

계속 .get, .getIn 하는거 싫다! 그렇다면 Record

Immutable 을 사용하시는 분들 중에서, Record 의 존재를 잘 모르시는 분들도 있습니다. (주관적인 생각입니다. 제가 그랬거든요. 비교적 최근 알게되었습니다.) Record 를 사용하면 Immutable 의 set, update, delete 등을 계속 사용 할 수 있으면서도, 값을 조회 할 때 get, getIn 을 사용 할 필요 없이, data.input 이런식으로 조회를 할 수 있습니다.

Record 는, Typescript 혹은 Flow 같은 타입시스템을 도입 할 때 굉장히 유용합니다.

자, 다시 우리가 아까전에 Immutable 연습을 했을 때 처럼, index.js 에서 Record 를 불러와서 연습해보겠습니다.

다음 코드의 주석을 하나하나 읽어가면서 직접 작성해보세요!

index.js

import React from 'react';
import { render } from 'react-dom';
import App from './App';
import { Record } from 'immutable';

const Person = Record({
  name: '홍길동',
  age: 1
});


let person = Person();

console.log(person); 
// ▶Object {name: "홍길동", age: 1 }

console.log(person.name, person.age);
// "홍길동" 1

person = person.set('name', '김민준');
console.log(person.name); // 김민준


// 이건 오류 납니다: person.name = '철수';

// Record 에서 사전 준비해주지 않은 값을 넣어도 오류납니다.
// person = person.set('job', 5);


// 값을 따로 지정해줄수도 있습니다.
person = Person({
  name: '영희',
  age: 10
});

const { name, age } = person; // 비구조화 할당도 문제없죠.
console.log(name, age); // "영희" 10

// 재생성 할 일이 없다면 이렇게 해도 됩니다.
const dog = Record({
  name: '멍멍이',
  age: 1
})()

console.log(dog.name); // 멍멍이

// 이런것도 가능하죠.
const nested = Record({
  foo: Record({
    bar: true
  })()
})();

console.log(nested.foo.bar); // true

// Map 다루듯이 똑같이 쓰면 됩니다.
const nextNested = nested.setIn(['foo', 'bar'], false);
console.log(nextNested);

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

Record 가 어떤식으로 작동하는지 조금 감을 잡으셨나요? 그러면, 우리가 만들었던 사용자목록을 Record 를 사용해서 구현해보도록 수정해보겠습니다. (기존에 index.js 에서 연습한 내용은 다시 지워주세요.)

App.js

import React, { Component } from 'react';
import UserList from './UserList';
import { Map, List, Record } from 'immutable';

// User 를 위한 Record 생성
const User = Record({
  id: null,
  username: null 
});


// Data 를 위한 Record 생성
const Data = Record({
  input: '',
  users: List()
});


class App extends Component {
  id = 3;

  state = {
    data: Data({
      users: List([
        User({
          id: 1,
          username: 'velopert'
        }),
        User({
          id: 2,
          username: 'mjkim'
        })
      ])
    })
  }

  onChange = (e) => {
    const { value } = e.target;
    const { data } = this.state;

    this.setState({
      data: data.set('input', value)
    });
  }

  onButtonClick = () => {
    const { data } = this.state;

    this.setState({
      data: data.set('input', '')
        .update('users', users => users.push(new User({
          id: this.id++,
          username: data.get('input')
        })))
    })
  }

  render() {
    const { onChange, onButtonClick } = this;
    const { data: { 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;

User 와 Data 를 위한 Record 를 사전준비 해주었고, onButtonClick 에서 기존에 data.get('input') 부분을 data.input 으로 바꿔주었습니다. (참고로 .get 도 문제없이 작동하기 때문에 무조건 이렇게 바꿔줄 필요는 없습니다.)

그리고, render 부분에서도 기존에 const input = data.get('input') 과 같이 일일히 레퍼런스를 만들어주었던 것을, 그냥 간편하게 비구조화 할당 해주었습니다.

이제, UserList 와 User 도 변경해주겠습니다. 이 또한 무조건 해야 할 작업은 아니지만, 기껏 Record 로 만들어주었는데, 굳이 .get() 을 쓸 필요는 없지 않겠어요?

UserList.js

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

class UserList extends Component {

  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.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(nextProps, nextState) {
    return this.props.user !== nextProps.user;
  }
  render() {
    const { username } = this.props.user;

    console.log('%s가 렌더링 되고있어요!!!', username);

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

export default User;

Edit 시작하세요 Immutable.js with Record
지금까지 한 작업은 위 버튼을 클릭하여 확인 할 수 있습니다.

정리하며…

리액트에서 state 를 업데이트 할 때에는, 언제나 불변함을 유지시켜주어야 합니다. 자칫 잘못했다간 나중에 최적화를 제대로 못하게 되는 일이 발생하죠.

Immutable.js 같은 도구를 사용하지 않아도, 그냥 자바스크립트 기능만을 사용하여 충분히 불변함을 유지 할 수 있습니다. 하지만! Immutable.js 를 사용하면 이러한 작업을 훨씬 빠르고 쉽게 할 수 있습니다. 특히 state 의 구조가 조금 복잡 할 때 사용한다면 더더욱 효과적입니다.

정말 간단한 애플리케이션이라면 사실상 그냥 ES6 기능만을 사용하여 구현하는 것도 충분하다고 생각합니다.

추가적으로 Immutable 은 Redux 와 함께 사용한다면 더더욱 빛을 발합니다! Redux를 통한 React 어플리케이션 상태관리 튜토리얼에서 Immutable 을 Redux 에서 사용하는 방법도 다루니, 관심이 있다면 참고해보시길 바랍니다.

  • Wonkun Kim

    immutable.js 의 사용법에 대해서 많이 배웠습니다. 감사합니다.

  • 쎄바

    안녕하세요~ 좋은 자료 잘 읽고갑니다 감사합니다^^
    한가지 궁금한게 있는데요, 예제 코드를 보면 비구조화 할당을 많이 사용하시는데요, 코드 가독성이 좋지 않아서 많이 안쓰는걸로 알고 있는데, 사용하시는 이유가 따로 있을까요??

  • 대박.. immutable이 이렇게 활용되는군요. 좋은 포스팅 정말 감사드립니다

  • sam

    setState 내에서 data: data.set(..)할 때 [eslint] ‘data’ is not defined. 에러가 나네요
    data: this.state.data.set(..)으로 바꿨습니다.

  • sam

    data.set()을 호출할 때
    [eslint] ‘data’ is not defined.
    에러가 나네요.

    this.state.data.set()으로 호출하니 에러가 없어졌어요

    • const { data } = this.state;
      이걸 상단에 작성하지 않은게 아닐까요~

  • 이용준

    강의 잘봤습니다:D 그런데 각 컴포넌트마다 shouldComponentUpdate의 인자가 prev가 아니라 next라고 해야 의미가 맞는 것 아닌가요?

    • 이런 커다란 실수를 … ㄷㄷ 자료 만들때 졸았나봅니다
      ㅋㅋㅋㅋ

      setState 가 될 때 users 를 따로 넣어주지 않으면 그대로 유지됩니다.

  • 원크(원크)

    감사하빈ㄷ,