작성
·
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)];
}
자세한 답변 감사드립니다 ㅎㅎ 처음 제 질문에 대해 답변 주신 리액티브한 특성에 관련한 내용은 이해가 되었는데, 마지막에 객체 복사본을 반환하는 코드로 바꾸신 이유가 있을까요?
기존 코드랑 어떤 의미의 차이가 있는지를 잘 모르겠습니다 ㅠ 정확히는 왜 이때 객체 복사를 발상해야 하는지를 모르겠습니다
리액트가 상태나 프롭의 변화를 감지하는 방식을 알면 이해하실 수 있을겁니다.
원시타입은 값이 다르면 다르다고 판단합니다.
가령 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>
);
}
코드의 master 브랜치에 변경한 내용도 참고 부탁드립니다.
https://github.com/jeonghwan-kim/lecture-react/commit/59ee9103462d82eb15f83402161337bac63c39ac