View on GitHub

FireFours

React Study: 발등에 불떨어진 4학년들의 모임

1월 3일

이번주 우리가 공부한 내용들

중요내용 정리와 질의 응답 정리

react-virtualized에서 List 컴포넌트의 height (p.307)

height는 렌더링된 할 일 리스트들의 높이를 모두 합친, 전체 리스트의 높이였다. (57 * 9 = 513) 즉, rowRenderer를 통해 만든 각 TodoListItem들의 높이를 모두 합친 길이이다.

rowRenderer함수에서 style의 역할 (p.308)

컴포넌트 최적화를 위해 react-virtualized에서 제공하는 List 컴포넌트를 사용했는데, 여기에서 rowRenderer 함수를 제공한다. rowRenderer 함수는 List 컴포넌트에서 각 TodoListItem을 렌더링하는 데 사용된다.

const TodoList = ({ todos, onRemove, onToggle }) => {
	const rowRenderer = useCallback(
		({ index, key, style }) => {
			const todo = todos[index];
			return (
				<TodoListItem
					todo={todo}
					key={key}
					onRemove={onRemove}
					onToggle={onToggle}
					style={style}
				/>
			);
		},
		[onRemove, onToggle, todos],
	);

	return (
		<List
			(...)
			rowRenderer={rowRenderer}
		/>
	);
};

이 rowRenderer 함수는 다양한 파라미터를 받아올 수 있는데, 그 중 style props를 받아와 TodoListItem에도 사용할 수 있다. TodoListItem 컴포넌트의 최상단의 태그에, props로 받아온 style을 적용시켜주면, 투두리스트가 잘 렌더링된다.

궁금했던 점은, 여기서 style이 어떤 역할을 하는지에 대해서였다. style을 지우고 리스트를 스크롤해보니, 아래의 다음 투두리스트들이 렌더링되지 않고, 기존의 위의 투두리스트들만 보여졌다.

고민하고 찾아본 결과, style은 행의 positioning을 위해 행에 적용되는 객체였다. 행의 사이즈와 위치를 정의해주기 때문에, 렌더링되는 행 요소에 반드시 넣어야 한다. 그래서 렌더링되는 행 요소인 TodoListItem 요소에 적용된 것이었다.

react virtualized의 리스트 컴포넌트에서 스크롤이 잘 작동하게 하기 위해서는, 위와 같이 style을 통한 sizing&positioning이 필요하다.

P.298 useState의 함수형 업데이트

useState에서 새로운 상태 값을 넣어 상태를 업데이트 시켜줄 수도 있지만, 아예 업데이트는 함수를 넣어줄 수도 있다. 이를 함수형 업데이트라 부르며 여러 장점이 존재한다. 비슷하게 함수형 업데이트에 대한 언급이 클래스 컴포넌트의 setState에서도 나온다. 이때 함수형 업데이트의 장점은 이전 값을 받아 update하는 것으로 setState의 비동기성 때문에 일어나는 문제에 대한 해결 방안이였다.
특히 책에서 소개하는 장점은 이 함수형 업데이트를 통해 성능을 크게 올릴 수 있다는 점이었다.

단순히 ()=> 를 추가하는 것으로 어떻게 크게 성능을 올릴 수 있었을까?

그 물음에 대해선 단순히 ()=> 를 추가해서 성능이 좋아진 것이 아니라 useCallback과의 시너지 덕분이었다고 답할 수 있을 것 같다.
onRemove 함수를 예를 들어보면, 이전엔 useCallback의 두번째 인자로 todos를 주어 todos의 값이 변할 때마다 새로운 함수를 생성해주었다.

const onRemove = useCallback(id => {
  setTodos(todos.filter(todo => todo.id !== id));
}, [todos]);

다른 state가 변해도 함수가 생성하지 않는다는 장점이 있었지만, 여전히 todos의 변화에 따라 함수를 새로 생성해야한다는 오버헤드가 존재한다.
그러므로 다음과 같이 함수형 업데이트를 사용할 수 있다.

const onRemove = useCallback(id => {
  setTodos(todos => todos.filter(todo => todo.id !== id));
}, []);

이렇게 되면 두번째 인자는 빈 배열로 주어 처음 mount될 때 생성된 이후로 새로 생성하지 않는다.

만약 함수형 업데이트가 아닌 아래 코드와 같이 그냥 빈배열로만 바꾼다면 todos엔 처음 초기값만 존재하고 이후에 업데이트되는 값을 존재하지 않게 된다.

const onRemove = useCallback(id => {
  setTodos(todos.filter(todo => todo.id !== id));
}, []); // 문제 발생, todos가 바뀌지 않음

함수형 업데이트는 이러한 문제를 해결하고 새롭게 업데이트되는 todos를 그때그때 함수로써 입력받아 실행이 가능하다.
위와 같이 함수형 업데이트는 useCallback의 함수 생성을 단 한번만 해도 되는 것으로 개선시켜준다. 비슷한 효과를 주는 것이 useReducer로 이것 또한 상태의 업데이트 로직을 분리하고 업데이트에 따른 생성을 막아 성능 개선을 꾀할 수 있다.

useState 여러 가지 유의점

useState에 대해 찾아보며 몇가지 사용의 유의점이 있었다.

1. State를 나눌 때는 주로 같이 update가 일어나는 것으로 묶어라

useState({row:10, col:20, tuple:200}) 이런 식으로 여러 개의 상태 값을 오브젝트 형식으로 하나의 state에서 관리할 수도 있고

useState(10), useState(20), useState(200) 이런 식으로 상태값을 모두 쪼개어 관리도 가능하다. 이에 대한 기준은 리액트 공식문서에서 설명하고 있다.

결론은 상태 업데이트에서 기존의 값을 복사하고 이를 업데이트 하는 것에 오버헤드가 크니, 주로 상태 업데이트가 같이 일어나는 상태값 위주로 묶어서 관리하라는 것이다. 너무 쪼개 놓아도 각각을 업데이트 하는 것에 오버헤드가 크고, 너무 하나의 몰아 넣는 것도 기존의 값을 복사하는 것에 오버헤드가 크므로 분리시켜주는 것이 중요하다.

2. event는 재사용될시 null로 변하므로 const로 값을 저장하라

setMessage(p => {
  return { ...p, message: e.target.value };
}); // Doesn't work

위와 같이 useState를 사용하면 다음과 같은 오류가 난다.

Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property `target` on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist(). See https://fb.me/react-event-pooling for more information.

공식 문서의 event pooling 설명에 따르면 synthetic event 객체 자체가 재사용되면서 모든 속성이 비워지기 (nullified) 때문에 이를 비동기적인 방식으로 사용하는 것은 불가능하다는 것이다. 여기서 비동기적인 방식이 setMessage가 되므로 이를 고치려면 미리 const로 해당 값을 저장하고 이를 업데이트 해야한다. 코드는 아래와 같다.

const msg = e.target.value;
setMessage(p => {
  return { ...p, message: msg };
}); 

Shallow copy and deep copy

:얕은 복사와 깊은 복사
목차
기본 개념

자바스크립트에서의 복사는 크게 두가지가 있다. 객체내부에 있는 객체들의 “참조”만 복사하는 shallow 복사와 immutable 한 “값”까지 복사하는 deep한 복사가 있다.

이는 평소 우리가 코딩을 배울 때 함수의 값 복사에서 사용하는 call by ref와 call by value의 개념을 생각해보면 이해가 쉽다. call by ref는 함수에서 변수를 읽어올 때 호출시 대입되는 변수의 참조값을 읽어와 변수에 직접 접근하여 값을 바꾼다. 반면 call by value 는 파라미터로 들어오는 변수가 저장하고 있는 ‘값’ 즉 데이터만 읽어오게 된다.

자바스크립트의 얕은복사와 깊은복사도 이 개념에 비교해서 볼 수 있다.

코드를 보며 알아보자

React.memo (리액트 성능 최적화)

React.memo는 고차 컴포넌트(Higher Order Component)입니다. 함수 컴포넌트가 동일한 props로 동일한 결과를 렌더링한다면, React.memo를 호출하고 결과를 메모이징(memoizing) 하도록 래핑하여 성능향상을 누릴 수 있습니다. 다시 말해, React는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용합니다. props가 갖는 복잡한 객체에 대하여 얕은 비교만을 수행하는 것이 기본 동작입니다. 다른 비교 동작을 원한다면, 두 번째 인자로 별도의 비교 함수를 제공하면 됩니다. 아래 참조
function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProp가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);
React.memo(Component, [areEqual(prevProps, nextProps)]);
function moviePropsAreEqual(prevMovie, nextMovie) {
  return (
    prevMovie.title === nextMovie.title &&
    prevMovie.releaseDate === nextMovie.releaseDate
  );
}

const MemoizedMovie2 = React.memo(Movie, moviePropsAreEqual);
특정 컴포넌트의 props를 비교를 하는 함수를 만들어 React.memo의 두번째 매개변수에 적용하면 리렌더링을 하기 전에 해당 props값의 변화 유무를 확인합니다.
하지만, 특정 props를 비교하는 areEqual 함수를 잘못 사용하면 버그가 발생할 수 있습니다. 예를 들어, 함수형 업데이트를 하지 않은 상태로 props의 특정 요소만 비교한다면 정작 props를 참조해야 하는 메소드에서 최신 props 배열이나 객체(갱신된 값이 반영된)를 참조하지 않으므로 심각한 문제를 야기할 수 있습니다.
  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

##### 위에 제시된 코드는 React.memo의 얕은 복사를 보여주는 코드입니다. if문을 통해 알 수 있듯이 React.memo는 주어진 key(props)에 대해 얕은 복사를 하고 있고 만약 key 속에 객체나 배열이 있을 경우 반영을 못 할 것으로 예상할 수 있습니다.

###### 참조 https://ko.reactjs.org/docs/react-api.html#reactmemo https://ui.toast.com/weekly-pick/ko_20190731/