[비정기적기록]렌더링 최적화가 필요한 이유

SANGHYUN KIM·2024년 1월 7일
1

비정기적 기록

목록 보기
3/6
  1. 직장에서 코드리뷰(라고 불리기에는 한 달에 한 번)시간에 공유한 내용을 다시 정리한 글입니다.
  2. 23년 9월, 23년 10월의 내용에 공통되는 부분이 있어서 하나로 합쳤습니다.
  3. UI는 Kendo UI를 활용합니다.
  4. React Fiber를 아직 보지 않았습니다. 다 발표하고 나서야 발견을 했습니다. 따라서 이번에는 React Fiber에 대한 내용을 포함하지 않았습니다.

상황

최근 2달간 회사에서 진행한 코드리뷰의 주제는 다음 2개다.

  1. input을 입력할 때마다 focus가 풀리는 현상
  2. kendo의 form을 이용하여 onSubmitClick 발동 시 변동값이 아닌 초기값으로 화면에 표시되는 상황

이 두 주제의 핵심은 "렌더링 최적화"였다.

1차 - focus가 풀리는 상황

// before
const Component = () => {
	const [ dependecy, setDependecy ] = useState()
    
	const InputCell = () => {

	};

	return(
		<Field component={InputCell} />
	)
}

TS 전환 및 리팩터링을 진행하는 와중에 위와 같은 페이지 컴포넌트를 만났다.
InputCell은 내부에 선언이 되어있고 focus값이 탈출을 하는 상황을 인지하고, 탈출을 막기 위해서 ref.current로 foucs를 유지를 하고 있었다고 한다.
해당 페이지를 TS로 전환하면서 최신화를 하기위해서 현상을 검색하다가 useCallback을 활용하면 focus가 풀리는 것을 막을 수 있다고 한다.
따라서 아래와 같이 수정을 하였고, 기존 ref.current를 활용하지 않고도 foucs를 유지할 수 있게 되었다.

// after
const Component = () => {
	const [ dependecy, setDependecy ] = useState()
    
	const InputCell = useCallback(() => {

	}, [dependecy])

	return(
		<Field component={InputCell} />
	)
}

2차 - form제출 시 초기값으로 롤백

상태만으로 관리하던 input값을 Kendo의 Form으로 전환시키는 작업을 했다.
모든 값을 선언해주고 api연결을 위해서 submit action 실행 시 input의 값들이 초기값으로 롤백이 되는 현상이 발생했다.
이 때는 위 1차 상황을 해결 한 이후 였기에 Form의 initalValues api에 할당하는 함수를 useCallback으로 감싸 주었다.
(Tanstack query의 select를 useCallback으로 래핑)

// useLpConfiguration 내부
export function useLpConfiguration({ queryKey }: { queryKey: CustomQueryKey }) {
  const { data: res, error, isSuccess } = useQuery(queryKey, Policy.getLpConfiguration, {
    select: useCallback(({ data }: { data: LpItem }) => convertSetLpData(data), [])
  });
  return {
    lpData: res,
    error,
    isSuccess,
  };
}

// 화면 로직
const { error, lpData } = useLpConfiguration({ queryKey: [QUERY_KEY.lp] });
const initialValues = { ...lpData };

return (
	<Form 
		initalValues={initalValues} // 초기값
		key={JSON.stringify(initalValues)} // 리렌더링 필요 여부의 판단값
		render=(.....)
	/>
)

두 상황 모두 렌더링 최적화를 하지 않았기 발생한 상황이다.

왜 렌더링 최적화를 해야하는 것인가?

먼저 두 상황 모두 리액트로 인하여 새로운 html로 계속 마운트되었기 때문이다.

  1. focus가 풀리는 것은 event가 발생하면서 함수가 다시 읽혀 새로운 html로 대체가 되었다.
  2. Kendo의 초기값이 변경되는 것을 감지하려면 key값을 통해서 변형시킬 수 있는데, select의 함수에 인해서 내부의 값이 변경되기에 다시 마운트되고 있었다.

그렇기에 내가 원하는 부분을 그대로 유지를 하기 위해서는 최적화 작업을 하여, 해당 부분이 특정 상황에서만 마운트 되게 해줘야 했다.

이런 부분은...

  1. useCallback을 통해서 동일한 참조값을 유지를 하거나
  2. 외부에서 import를 하거나
  3. 같은 파일 내에서 다른 함수로 작성하여 함수가 읽히는 와중에 다르게 인식을 해줘야 했다.

근데 왜 이렇게 해줘야만 할까? 그냥 작성하면 되지 않았을 까?
결국 위와 같이 해야 하는 이유는 리액트의 렌더링 과정을 보고 이해할 수 있었다.

이 기회가 없었다면 최적화에 대한 이유를 찾지도 않았을 것 같다.
최적화가 필요한 이유를 알기 위해선 리액트의 동작방식에 대한 이해가 필요했다.

리액트는 어떻게 렌더링하고 컴포넌트를 대체하는가?

공식문서 및 몇몇 개발 글들을 조합했을 떄 부가설명(내부 동작 원리) 없이 간단하게 요약하자면 다음과 같이 정리를 할 수 있었다.

  1. 초기 렌더링: React는 자바스크립트를 사용하여 React DOM 생성 후 실제 DOM에 반영
  2. 리렌더링 조건: props 또는 state가 변경또는 useEffect 실행에 따른 리렌더링 유발
  3. 리렌더링 대상 인식: 리렌더링 조건이 발생된 컴포넌트를 '리렌더링 대상'으로 인식
  4. Diffing 알고리즘: '리렌더링 인식'이된 컴포넌트는 React의 heuristic 기반 diffing 알고리즘에 의해 평가(같은 tree level에서 변경점을 찾아내며, 다른 tree level에 대해서는 비교X)
  5. React DOM 업데이트: 평가 이후 다르다고 판단되면 해당 노드 기점 하위 트리 함수들을 전체 실행하여 결과 값을 React DOM에 업데이트
    6.DOM 업데이트: 변경된 React DOM을 기반으로 웹브라우저 DOM에 업데이트

여기서 내가 확인해야할 부분은 2번과 3번이었다.

리렌더링 조건 유발

내가 겪은 문제들은 다음 조건으로 인해 리렌더링 조건을 만족했다.

  1. Kendo Input값은 onChange 이벤트가 있는데 해당 이벤트로 Kendo 내부 상태가 변경되기에 리렌더링 조건 만족
  2. Kendo Form 제출 시, 함수가 다시 읽히며 리렌더링 조건 만족

따라서,
1. 화면 파일이 다시 재평가되면서 컴포넌트 본문에 선언된 모든 변수는(Kendo Input) 다른 함수라고 인식을 하기에 화면에 다시 업데이트 필요라고 인식
2. 제출 시, 함수가 다시 재평가되면서 key로 가지고 있는 query의 data가 변경되면서 이전과 다르다고 인식하여 화면 업데이트

결국 나는 내 상황에서 해당 값들은 재평가 될 때 특정 상황이 아닌 경우 이전과 같은 값이라고 React에게 전달해줘야 했다.

받은 질문

1. 렌더링 최적화도 좋은데, dom 또한 최적화를 진행하시나요?

처음 이해가 되지 않는 질문이었다. 그래서 다시 의도를 여쭤보니 "window 객체에 붙어 있는 event들과 virtual DOM이 기존 tree를 끊어내고 다른 tree를 붙였을 때 기존 tree에 대한 메모리 삭제를 진행하는지"에 대해서 궁금하셨다.

솔직하게 모른다고 했다.

시니어 개발자분의 따르면 dom자체가 최적화되기 어렵고 JavaScript의 garbage collector또한 성능이 타 언어에 비해 좋지 않다고 한다. 그럼에도 살아남아 계속 사용하게 되는 이유는 "우리가 브라우저가 느리다고 인식이 되면 새로고침을 통해서 메모리를 초기화시키는 행동"을 하는 것이 하나의 원인이 될 수 있다고 한다.
또한, event들이 전역 객체에 붙여 있기에 event listener 떼어주는 행위도 필요하다고 한다.

지금까지 수많은 글에서 useEffect 사용 시 return문에 event를 왜 해지하는 지 몰랐는데, 이 질의를 통해서 그 코드의 사용성을 이해했다.

끝맺음

처음 개발 공부할 때 생명주기를 알려주는데 class기반의 내용이고 hook이 이것을 함수에서 할 수 있도록 도와준다고 하는데, 이해를 해본 적이 없었다. 왜냐하면 함수기반 생명주기가 class의 모든 것과 부합하지 않았기 떄문이다.
그러나 이번 기회에 어떻게 함수기반 컴포넌트의 렌더링을 파악하고 최적화가 필요한 상황이 왜 생겼는지를 알게되었다.

이후 블로그로 다시 글을 옮기게 되면서 내가 위에서 공부한 내용이 예전 내용이라는 것을 알게되었다.
현재는 Fiber라는 새로운 알고리즘을 채택하여 리액트를 변화시켰다고 한다.

다음 공부해야 할 것에 Fiber도 추가해야겠다. 또한, 이번 2달간의 발표로 브라우저에 대한 공부도 더 필요하다는 생각이 들었다.

참고

재조정 (Reconciliation) – React

state 보존 및 재설정 – React

리액트는 어떻게 작동할까 Diffing - 3주차 회고

profile
꾸준히 공부하자

0개의 댓글