인프런 커뮤니티 질문&답변

이재은님의 프로필 이미지
이재은

작성한 질문수

[리액트 1부] 만들고 비교하며 학습하는 리액트 (React)

[추천/최근 검색어] 최근 검색어 3(풀이)

최근검색어 3 풀이에서

작성

·

108

1

안녕하세요 선생님 수업 잘듣고 있습니다.

다소 수준이 높은 수업이라 조금 헤매고 있지만요..

최근 검색어 3에서 2분 41초 경에

store js에서 15번째 줄

search(keyword) {

this.addHistory(keyword);

return this.storage.productData.filter((product) =>

product.name.includes(keyword)

);

}

이 부분이 있는데요. 이 부분때문에 store의 search가 addHistory를 호출하고 상태변화가 이루어질 수 있는데 (addhistory 내용만 수정하면 main.js의 코드를 수정안해도 목표로 하는 최근 검색어에 목록 추가를 할 수 있어서요)
굳이 4분 1초 경에 main.js의 search에서 setstate를 건드리시는 이유가 무엇인가요?

이미 상태변화가 store.js에서 작성한 코드 때문에 이루어지고 있기때문에 다소 역할이 중복된 코드가 아닌가해서요

답변주시면 감사하겠습니다

답변 1

0

김정환님의 프로필 이미지
김정환
지식공유자

그러네요. 말씀하신것 처럼 Store의 addHistory()만 호출해도 실습 풀이를 할 수 있을 것 같습니다.

두 가지를 말씀드리고 싶어요.

 

1. addHistory()는 리액트 상태 변화를 일으키지는 않습니다.

  • 상태는 컴포넌트의 setState()로 변경해야 합니다. 그래야 컴포넌트가 다시 그려지기 때문입니다.

  • Store의 addHistory()는 멤버 변수인 storage 객체의 일부 값만 바꿉니다. UI를 다시 그리지는 않습니다.

  • 이 값을 컴포넌트 상태에 반영해야 리액트가 바뀐 값을 감지하고 다시 그릴겁니다. (풀이의 의도)

 

2. 그럼에도 불구하고 말씀하신 코드가 동작하는 이유

  • 컴포넌트의 search()에서 setState를 호출하면 render()가 호출됩니다. (historyList 를 빼도 마찬가지에요)

  • render() 에서는 this.state.historyList 상태로 UI를 그리는데요,

    historyList는 componentDidMount에서 store.getHistoryList로 얻은 값입니다.

  • 이것은 Store가 가진 historyData 객체의 레퍼런스입니다. 이 부분이 말씀하신 코드가 동작하는 원인입니다.

  • 컴포넌트는 이 레퍼런스를 기억하고 있어서 누군든지 이 레퍼런스가 가리키는 값을 수정하면 컴포넌트는 수정한 값을 사용할 겁니다. store.addHistory()가 이 객체를 수정하면 컴포넌트는 render()에서 레퍼런스로 변경된 객체 값을 사용합니다.

 

이 수업은 리액트의 리액티브한 특성을 알려드리는 것이 의도입니다.

  • 상태를 변경하기만 하면 UI에 반영되는 성질

  • 그래서 컴포넌트 search()에서 historyList 상태를 변경해 UI를 알아서 갱신하는 의도로 코딩했습니다.

 

store 멤버 변수의 레퍼런스를 컴포넌트 상태로 사용하면서 수강자분께 혼란을 드린것 같습니다. 이 부분은 store의 조회 메소드를 수정해 레퍼런스가 아니라 <복사 값을 반환>하는 로직으로 변경하면 좀 더 명확할 것 같습니다.

 

addHistory(keyword = "") {
  // (...)

  // 새로운 객체를 만듭니다.
  this.storage.historyData = [
    ...this.storage.historyData.sort(this._sortHistory),
  ];
}

getKeywordList() {
  // 객체 복사본을 반환합니다.
  return [...this.storage.keywordData];
}

getHistoryList() {
  // 객체 복사본을 반환합니다.
  return [...this.storage.historyData.sort(this._sortHistory)];
}
김정환님의 프로필 이미지
김정환
지식공유자

코드의 master 브랜치에 변경한 내용도 참고 부탁드립니다.

https://github.com/jeonghwan-kim/lecture-react/commit/59ee9103462d82eb15f83402161337bac63c39ac

이재은님의 프로필 이미지
이재은
질문자

자세한 답변 감사드립니다 ㅎㅎ 처음 제 질문에 대해 답변 주신 리액티브한 특성에 관련한 내용은 이해가 되었는데, 마지막에 객체 복사본을 반환하는 코드로 바꾸신 이유가 있을까요?

기존 코드랑 어떤 의미의 차이가 있는지를 잘 모르겠습니다 ㅠ 정확히는 왜 이때 객체 복사를 발상해야 하는지를 모르겠습니다

김정환님의 프로필 이미지
김정환
지식공유자

리액트가 상태나 프롭의 변화를 감지하는 방식을 알면 이해하실 수 있을겁니다.

원시타입은 값이 다르면 다르다고 판단합니다.

  • 가령 1과 1은 같은 값입니다.

  • 하지만 1과 2는 다른값입니다.

한편 합성타입은 참조가 바뀌어야 다르다고 판단합니다.

  • 가령 {}과 {}은 다른 값입니다. 객체 구성은 같지만 객체의 참조값이 다르기 때문입니다.

  • 물론 {}과 {a: 1}도 다른 값입니다. 여전히 각 객체의 참조값이 다르기 때문입니다.

리액트는 상태나 프롭을 비교할 때 이 원칙으로 비교합니다. 우리가 사용한 Store의 조회 메소드는 배열을 반환하는데요. 합성타입입니다.

  • this.storage.historyData를 반환하면 참조값을 반환합니다(기존).

  • 리액트는 이 값만 보고 비교하기 때문에 아무리 historyData 배열의 항목을 추가하거나 빼더라도 변경되었다고 판단하지 않습니다.

  • [...this.store.historyData] 를 반환하면 새로 만든 배열의 참조 값을 반환합니다(변경).

  • 리액트는 기존 참조값이 새로운 값으로 바뀌었다고 판단합니다. 리렌더합니다.

이 설명을 간단히 보여줄 예시 코드입니다. 참고하시면서 도움이 되였으면 좋겠어요.

function App() {
  const [objectState, setObjectState] = React.useState({id: 1});
  const [stringState, setStringState] = React.useState('hello');

  const changeStringState = () => {
    setStringState('world') // App이 다시 호출됩니다.
  }

  const changeObjectState = () => {
    // App이 다시 호출되지 않습니다.
    // objectState.id를 바꾸었지만 objectState의 참조값은 변하지 않았기 때문입니다.
    // setObjectState는 objectState가 바뀌었다고 판단하지 않습니다.
    objectState.id = 2
    setObjectState(objectState) 

    // setObjectState에게 새로운 객체(객체 복사본)을 전달합니다.
    // 다른 객체이기때문에 참조 값도 다릅니다.
    // 리액트는 다른 객체값이 바뀌었다고 판다하고 리렌더할 것입니다.
    // setObjectState({id: 2})
    
  }


  console.log('App rendered')

  return (
    <ul>
      <li>
        stringState: {stringState}
        <button onClick={changeStringState}>changeStringState</button>
      </li>
      <li>
        objectState: {JSON.stringify(objectState)}
        <button onClick={changeObjectState}>changeObjectState</button>
      </li>
    </ul>
  );
}
이재은님의 프로필 이미지
이재은

작성한 질문수

질문하기