(React) 3. React Immutability & Spread Operator(...)

김동우·2021년 7월 29일
0

wecode

목록 보기
20/32
post-thumbnail

잠깐! 시작하기 전에

이 글은 wecode에서 실제 공부하고(이제 사전 스터디는 아닙니다.), 이해한 내용들을 적는 글입니다. 글의 표현과는 달리 어쩌면 실무와는 전혀 상관이 없는 글일 수 있습니다.

또한 해당 글은 다양한 자료들과 작성자 지식이 합성된 글입니다. 따라서 원문의 포스팅들이 틀린 정보이거나, 해당 개념에 대한 작성자의 이해가 부족할 수 있습니다.

설명하듯 적는게 습관이라 권위자 발톱만큼의 향기가 날 수 있으나, 엄연히 학생입니다. 따라서 하나의 참고자료로 활용하시길 바랍니다.

글의 내용과 다른 정보나 견해의 차이가 있을 수 있습니다.
이럴 때, 해당 부분을 언급하셔서 제가 더 공부할 수 있는 기회를 제공해주시면 감사할 것 같습니다.


서론

벌써 4주차에 접어든 위코드 생활입니다.

오늘 포스팅은 어제 저녁부터 저를 괴롭혔던 내용으로 가겠습니다.

Immutability

React는 불변성을 지킬 것을 요구하는 라이브러리입니다.

성능에 대한 문제를 개선하기 위해 불변객체를 사용하기 때문인데, 불변객체와 해당 내용은 공식문서의 엘리먼트 렌더링을 참고하시면 됩니다.

그렇다면 불변성을 고려하는 방법은 무엇이고, React는 불변성을 어떻게 지켜내고 있는지 생각해봅시다.

직접 접근하지 않는다.

React는 다양한 컴포넌트로 구성되고, 해당 컴포넌트는 상태인 state를 가지고 있을 수도 있고, 어쩌면 없을 수 있습니다.

또한 state를 갖는 컴포넌트의 경우, 제각기 다양한 구조로 state를 구성합니다.

그러나 예외 없이 모든 컴포넌트의 state에 공통적으로 적용되는 속성이 바로 불변성인데, 불변성을 고려하기 위해서는 단 하나의 조건만 충족하면 됩니다.

바로 객체로 이루어진 state의 값을 직접 변경하지 않는다는 조건입니다.

state 내부의 값을 직접적으로 변경할 경우, 결과적으로 React는 state가 변경된 것으로 인식할 수 없습니다.

그 이유는 개념에 가까운데, 불변객체는 참조만을 복사하기 때문입니다.

이전 JS 개념에서 내부 메모리 저장 구조를 뜯어보며 생각했던 그 내용입니다.

결국 데이터가 주소가 존재하는 프로퍼티 주소를 할당받는 것이 참조 복사의 과정인데, 이러한 복사의 경우 내부에 데이터가 위치한 주소가 달라진다고 해서 객체가 바라보고 있는 주소가 달라지지 않는다는 것이 포인트가 됩니다.


let obj = {testArr : [1, 2, 3, 4, 5]};

obj[`testArr`][2] = 4;

console.log(obj)

4로 바뀐 teatArr[2] 는 중복되는 기본형 데이터가 메모리 내에 존재하기에 4와 같은 주소를 할당받았을겁니다.

기존의 3은 처리가 되었고, 이 과정은 쉽게 이해할 수 있습니다.

그러나 결과적으로 teatArr가 참조하고 있는 영역의 주소는 달라지지 않았으며, obj 역시 참조하고 있는 영역의 주소가 달라지지 않았습니다.

이는 state 객체 내부에 접근해서 직접 값을 수정한다고 state의 참조 주소가 달라지지 않는것으로 생각할 수 있습니다.

그렇기에 직접 값을 수정하는 구문을 setState 내부에서 작성하더라도 state 간의 변경을 참조값 비교로 인식하고 있는 React의 경우 '변경사항 없음.' 판단을 내리게 됩니다.

이러한 불변성을 잘 이해하고, state를 원활히 바꾸기 위해서는 변경점이 있는 데이터 구조를 그대로 복사해서 새로운 데이터 주소와 참조를 가지는 새 state 객체를 반환해주면 됩니다.

여기서 concat(), assign(), (...arr(obj), )가 등장하게 됩니다.

Spread Operator, 전개구문(...)

전개구문이라는 말만 봤을 때, 역시나 조금 난해하다고 생각이 듭니다.

저는 그냥 펼침, 혹은 전체에 대하여~ 라는 식으로 이해하고 있습니다.

해당 연산자를 활용하면, 객체나 배열 전체에 대한 연산을 다양한 방식으로 표현할 수 있습니다.

그런 것은 뒤로 해두고, 우선 이 ...이라는 녀석이 왜 React에서 유의미한 영향을 끼칠 수 있는지 생각해야 합니다.

이유는 ... 연산의 결과는 원본 구조를 유지하는 새로운 객체로 반환되기 때문입니다.

그렇다면 이제 감이 좀 오는 것 같습니다.

...를 사용하면 우린 React의 불변성을 고려한 코드를 짤 수 있게 됩니다.

정확하게는 객체의 얕은 복사가 아닌 깊은 복사를 반복문 -> ...을 통해 진행할 수 있게 됩니다.

기존의 state 내부에서 변화된 값을 가진 새로운 state 객체를 반환할 수 있게 되는 것이죠.

더군다나 각 state는 다른 참조를 가지기에 setState() 메서드 호출 이후에 React는 다른 state임을 인지하고, render()를 호출할 수 있습니다.

문제는 구조가 복잡해질 때, 해당 방법도 완벽한 해답이 될 수 없다는 점입니다.

그러나 현재 저의 수준에서는 거의 다 해결됩니다.

예시코드로 정말 그런지 확인해봅시다.


let obj = {id:`1`, name : `dongwu`};

console.log(obj);

let newObj = {...obj};

console.log(newObj);

let lastObj = {...newObj, id : 2, name:`동우`}

console.log(lastObj)
  1. newObj는 obj의 구조와 내용을 빠짐없이 가지고 있는 새로운 객체입니다.

  2. lastObj의 경우 obj를 복사하여 새로운 참조를 갖는 newObj의 복사본입니다.

  3. lastObj 또한 newObj와 같이 새로운 참조를 갖기에 내부 값을 변경하는 듯 보이는 위 연산에서도 원본에 영향을 끼치지 않습니다.

  4. 애초에 변경된 내부 데이터의 주소를 새로 할당해준 다른 객체이기 때문에 참조하고 있는 주소가 다르게 됩니다.

  5. 이후 직접 접근으로 lastObj 값을 바꿔도 복사 이전의 원본에는 아무 영향이 없습니다.

즉, {...newObj, ~}연산 자체가 원본에 직접 접근해서 값을 바꾸는 것이 아니고, assign에 해당합니다.

이런 복사-변경을 ...을 이용하면 쉽게 표현할 수 있게 됩니다.

그 외에도 ...을 활용하면 다양한 메서드를 대체할 수 있습니다.

자세한 내용은 위 전개구문 단어를 클릭해서 확인해주세요.

이제 state를 새로운 state로 갈아끼우는 방법을 확인했습니다.

그럼 위 두 내용을 적용한 코드는 어떻게 나오는지 확인해보시죠.

React Code 예시

먼저, 스스로 인스타그램, 페이스북과 같은 클론을 구현해야 하는 상황이라고 가정합시다.

피드와 코멘트에서 유저와 상호작용해야 하는 이벤트는 다양합니다.

그 중, 좋아요에 해당하는 코드를 작성해야하는 상황이 왔고, 좋아요 카운트를 어떻게 구현할지 고민해야 합니다.

좋아요 카운트를 구현하기 위해서는 크게 4가지를 고려해야 합니다.

  1. 좋아요 카운트를 보유한 state의 위치

  2. fetch data 구조

  3. 좋아요 이벤트가 발생하는 컴포넌트가 그려지는 방식

  4. 카운트 추가와 조건

차근차근 예시 모델을 보며 생각해보겠습니다.

좋아요 데이터를 보유한 state의 위치

먼저, 우리의 component 중 좋아요 데이터를 state로 관리하는 컴포넌트가 어디인지 확인해야 합니다.

보통 feed(or comment)에 대한 데이터를 관리하는 component가 될테고, feed는 외부의 데이터일테니 좋아요도 외부에서 가져오는 데이터일겁니다.

그럼 feed data를 fetch하는 곳이 state-setState() 비교 위치가 됩니다.

저의 경우에는 feed 데이터를 관리하는 Main 이라는 컴포넌트네요.


class Main extends React.Component {
  constructor() {
    super();
    this.state = {
      feeds: [],
    };
  }

  componentDidMount() {
    fetch(`http://localhost:3000/data.json`)
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        this.setState({ feeds: data.feeds });
      });
  }

  render() {
    return (
      <div>
        {this.state.feeds.map((elem, idx) => {
          return (
            <Test
              key={elem.id}
              index={idx}
              like={elem.like}
            />
          );
        })}
      </div>
    );
  }
}

이제 위치를 특정했으니, 해당 컴포넌트의 fetch 데이터를 확인해봅시다.

fetch-data 확인


//data.json
{
  "feeds": [
    { "id": 1, "like": 123 },
    { "id": 2, "like": 333 },
    { "id": 3, "like": 222 }
  ]
}

위와 같은 형태일 때, componentDidMount(fetch()) 이후 Main의 state는 다음처럼 바뀔겁니다.


class Main extends React.Component {
  constructor() {
    super();
    this.state = {
      feeds: [
        { "id": 1, "like": 123 },
        { "id": 2, "like": 333 },
        { "id": 3, "like": 222 }
      ],
    };
  }
  ...

좋아요 이벤트가 발생하는 컴포넌트가 그려지는 방식

이제 이 데이터를 가지고 우린 자식 component를 구성합니다.

fetch data는 배열구조니까 Array.map()을 활용하면 될 것 같습니다.

//render()
...
{this.state.feeds.map((elem, idx) => {
    return (
      <Test
        key={elem.id}
        index={idx}
        like={elem.like}
      />
    );
  })
}
...

위와 같은 값들을 props로 넘겨주고 있네요.

이제 우리가 이용할 값은 index와 like입니다.

해당 Test component에서는 클릭 이벤트를 관리할거고, 클릭이 발생했을 때 like count가 올라가면 되겠군요.

그럼 그에 맞게 자식과 부모의 메소드를 구성해봅시다.

카운트 추가와 조건

먼저 자식을 부모의 render() 내에 만들었으니, 자식의 컴포넌트의 구조를 봅시다.


import React from "react";

class Test extends React.Component {
  handleClick = (e) => {
    this.props.addLikeByFeedIdx(this.props.index);
  };

  render() {
    return <span onClick={this.handleClick}>{this.props.like}</span>;
  }
}

export default Test;

handleClick 메소드 내에 props로 전달받은 메소드가 있습니다.

해당 메소드는 아래와 같습니다.


  addLikeByFeedIdx = (idx) => {
    this.setState((prevState) => {
      let newFeed = prevState.feeds.map((post, i) => {
        if (idx === i) {
          return { ...post, like: post.like + 1 };
        } else {
          return post;
        }
      });
      return { ...prevState, feeds: newFeed };
    });
  };

... Operator로 부모의 state를 변경하는 메소드인데, 이벤트 발생 컴포넌트인 자식에게 props로 넘겨주고 있습니다.

해당 방식은 자식에서 부모의 state에 접글할 때 사용하는 방식입니다.

state 내부 데이터를 props로 넘겨주고, 자식의 state에 props를 할당하는 방식이 아닌, 부모 state를 수정하는 메소드를 넘겨줍니다.

이를 통해 state는 해당 컴포넌트의 고유한 상태가 될 수 있기에, 많은 분들이 이러한 방식을 통해 자식에서 부모 state를 수정합니다.

먼저, 배열인 feeds에 Array.map()을 적용하고, 내부 객체인 post 변경값에 assign을 적용하듯 ... Operator를 활용하고 있습니다.

결과적으로는 이전 state인 prevState의 feeds의 value로 변경된 newFeed를 assign한 뒤, 변경된 state로 현재의 state를 set 합니다.

이렇기에 저는 갈아끼운다는 표현을 사용합니다.

원본에 대한 변경이 아니기에 그렇게 생각하면 이해가 쉬운 것 같습니다.

위와 같은 과정을 거치면, 클릭에 의해 좋아요 카운트가 +1 되는 기능을 구현할 수 있습니다.

좋아요는 한 사람당 한 번 누르는 것만 가능하니, post 내부에 boolean 값을 value로 갖는 liked 라는 property를 만들어주면 해결할 수 있을 것 같습니다.

테스트 결과는 없지만, 아래처럼 작성해주면 될 것 같습니다.


  addLikeByFeedIdx = (idx) => {
    this.setState((prevState) => {
      let newFeed = prevState.feeds.map((post, i) => {
        if (idx === i) {
          return !post.liked
            ? { ...post, like: post.like + 1, liked: true }
            : { ...post, like: post.like - 1, liked: false };
        } else {
          return post;
        }
      });
      return { ...prevState, feeds: newFeed };
    });
  };

이렇게 하면 좋아요의 횟수 제한과 실제 좋아요 카운트에 영향을 끼치는 코드를 작성하게 됩니다.

모든 것은 자식의 span의 클릭(onClick event)에서 trigger되고, 실제 영향은 부모의 state에 끼치게 되는 코드입니다.

마치며

React는 하면 할수록 알던 내용에 대해 다시 한 번 증명해보는 시간을 주는 것 같습니다.

state-props에 대해 이제 조금은 안다고 생각했었는데, 마냥 그렇지는 않다는 생각이 들 정도로 이번 내용은 오랜 고민을 요구했었습니다.

이제는 불변성보다는 fetch와 promise에 대해 개념을 잡아볼까 합니다.

그럼 이번 글은 여기서 마치겠습니다.

읽어주셔서 감사합니다.

0개의 댓글