컴포넌트 작성 피드백 및 리액트에서 렌더링 및 UseEffect 파해치기

2023. 5. 18. 16:50프론트엔드/ReactJS

컴포넌트 작성 피드백 

  • 코드/함수는 단순하고 한가지 일만 해야한다.
    • 컴포넌트가 한가지 일만 하게..?
  • 조건식 단순하게 만들기
    • 분기가 생기는 순간 코드 읽기가 복잡해진다. → 최대한 보기 좋게 처리하는것이 좋다.
    • 내가 처리하기 제일 편한 자료구조를 가져오는것이 기본 → 웬만하면 자료구조 선에서 처리되는게 훨씬 좋다.
    https://medium.com/javascript-scene/nested-ternaries-are-great-361bddd0f340
  • 컴포넌트가 너무 많은 요소들에 의존하고 있음
    • 컴포넌트의 해야하는 역할을 잘 생각해보자
    • 한 컴포넌트가 2가지 일을 처리하고 있는건 아닌지 생각을 해보자
    • 컴포넌트에 의존되는 값이 있는지 잘 생각해보고 거기에 예상되는 효과가 있는지 잘 생각해보기
  • 자주 쓰는 string, code, ment는 따로 빼서 사용하자
    • 중복을 줄이고, 오타를 발생시킬 가능성을 줄일 수 있다.
    • 변수에 의미를 부여하고 가독성을 높일 수 있다.
  • 불필요한 state 사용을 줄이자
    • state는 적을 수록 좋다 → 내가 관리해야할 변수가 적어진다. → 코드가 적어지고 읽기 쉬워진다.
      • props를 통해 전달 된다 → 확실히 state가 아니다.
      • 시간이 지나도 변하지 않는다. → 확실히 state가 아니다. → 상수로 사용하면 된다.
      • 컴포넌트안의 다른 state나 props를 가지고 값의 계산이 가능한가? → state가 아니다. → state가 다시 변경 되면서 변수가 다시 할당 될것이니까
  • 불필요한 복잡성은 제거하자.
  • 주석.
    • 모든 코드에 주석을 다는것이 좋을까?
      • 주석이 불필요한 코드
        • 이 코드를 주석을 달 정도로 읽기 불편한가? → 개선이 필요하다는 신호
        • 옛날 코드를 주석 처리 해놓은 코드
        • 존재 자체가 불필요한 코드
      • 주석이 필요한 코드는 뭘까?
        • 코드로 설명할 수 없는 부분
        • 나중에 해야할 일이 필요할 때
          • // todo: // fixme :
        • deprecated 될 코드
        • 파라미터 타입 , jsDocs →` ts로 해결 가능
        • 정규 표현식
  • 불필요한 함수 선언 - 5팀 피드백 !
    const fetchTodoRequest = useCallback(async () => {
      const res = await todoApi.fetchList();
      setTodoList(res);
    }, []);
    
    useEffect(fetchTodoRequest, [fetchTodoRequest]);
    
    • 성능상의 차이는 없지만, 읽는 사람의 차이가 있는것 같다.

React 렌더링 최적화

렌더링

  • 렌더링은 화면에 특정한 요소를 그려내는 것을 의미.
  • 브라우저 렌더링이란 결국 DOM요소를 계산하고, 그려내는 것을 의미.
    1. HTML과 CSS를 통해서 만들어 지고 계산된 DOM과 CSSOM의 결합
    2. 위치를 계산하고, 최종적으로 브라우저에 그려짐
  • DOM API를 JS를 통해 호출하면서 브라우저에 그려진 화면을 변화 시킴

선언적 언어

  • vanila js 에서 DOM에 직접 접근하고 수정하는 것 ( 명령형 )
    • 이를 최적화 하는 것은 어플리케이션의 규모가 커지면 커질수록 관리하기 힘들어짐
  • 개발자들은 애플리케이션에 부여주고 싶은 핵심 UI를 선언하기만하면 실제로 DOM을 조작해서 UI를 그려내고 변화 시키는 일은 라이브러리나 프레임워크가 대신 해주는 방식을 찾게 됨
    • React, Vue, Angular ⇒ “ 선언형 “
  • React는 선언형 → 실제 렌더링 과정은 React에서 대신 처리해줌 개발자는UI설계 하는대만 집중하면 됨.
  • 때로는 React내부에서 처리해주는 렌더링을 최적하 해야하는 상황이 발생.
    • React 내부에서 렌더링이 언제 발생하는지, 어떤 과정을 거쳐서 이루어지는지를 이해하고 있어야함

리액트에서의 리렌더링

  • react에서 리렌더링은 언제 발생 ? → 근본적으로 state를 왜 사용할까?

리액트에서 state를 사용하는 이유는 UI와 상태 (state)를 연동시키기 위해서

  • UI는 어떤 데이터가 있고, 그것을 보기 편한 형태로 표현한 것
  • 리액트는 UI와 연동되어야 하고 변할 여지가 있는 데이터를 state라는 형태로 사용
    • 데이터가 변경되었을 떄 ui가 그에 맞취서 변화하기 위해서 state를 변경시키는 방법을 제한 시키고, 이 함수가 호출 될 때마다 리렌더링이 되도록 설계
    • 리액트에서 리렌더링이 발생하는 시점 → state가 변했을 때
      • 해당 컴포넌트와 해당 컴포넌트 하위 컴포넌트들은 모두 리렌더링 된다.
        • → 명확한 멘탈 모델을 이해하고 있어야함

렌더링 과정

  1. 기존 컴포넌트의 UI를 재사용할 지 확인
  2. 함수 컴포넌트 : 컴포넌트 함수를 호출함 / class 컴포넌트 : render메소드를 호출
  3. 2의 결과를 통해서 새로운 VirtualDOM을 생성함
  4. 이전의 VirtualDOM과 새로운 VirtualDOM을 비교해서 실제 변경된 부분만 DOM을 적용
  • 😒 왜 4번의 과정을 거치는가? 브라우저의 근본적으로 화면을 보여주기 위한 방법은 HTML, CSS, JS를 다운 받고 화면에 픽셀형태로 그려냄 ⇒ CRP (critical rendering path)
  • 첫 번째의 렌더링이 끝나고, 이후 DOM또는 CSSOM이 수정될 때마다, 렌더링과정을 반복하게 된다. → react에서 이 중복된 과정(CRP을 최적화 하기 위해)을 해결한 부분이 VirtualDOM 인 것

리액트에서는 UI변화 가 발생하면 변화에 필요한 DOM조작들을 매번 바로 실제 DOM에 적용하는 것이 아니라 VirtualDOM과 새로운 VirtualDOM을 비교해서 실제로 변화가 필요한 DOM요소들을 찾아냄, 그 다음에 한번에 해당 DOM요소들을 조작한다. ⇒ 리액트가 알아서 해주는 부분

리액트 개발자가 할 수 있는 최적화

  1. 기존의 컴포넌트 UI를 재사용할 지 ?ex) React.memo
  2. 만약 리렌더링 될 컴포넌트의 UI가 이전의 UI와 동일하다고 판단되면, 새롭게 컴포넌트를 렌더링 하지 않고 이전의 결과값을 그대로 사용함
  3. 2의 결과를 통해서 새로운 VirtualDOM을 생성할 지?ex) <div className="block" /> 을 <div className="inline"> 으로 변환시키는 것 VirtualDOM끼리 비교했을 때 차이가 적은 형태로 만들어지도록 하는 것입니다.
  4. 컴포넌트 함수가 호출되면서 만들어질 VirtualDOM의 형태를 비교적 차이가 적은 형태로 만들어지도록 하는 것입니다.

React advanced Hooks

React.memo

state가 변한 컴포넌트의 경우 당연히 UI의 변화가 있어서 리렌더링을 해야함

  • 하위 컴포넌트의 걍우에는 props가 변화하지 않았다면 해당 컴포넌트의 UI가 변화 하지 않았을 수도 있을것 → 굳이 새로운 컴포넌트 함수를 호출할 필요없이 이전의 저장되어 있던 결과를 그래도 사용하는것이 효율적
  • 기존의 컴포넌트의 UI를 재사용할 지 판단하는 방법
  • 컴포넌트의 이전 props와 다음 렌더링 때 사용될 **props를 비교하고 차이가 있을 경우에만 리렌더링을 수행**, 만약 차이가 없으면 이전의 렌더했던 값을 사용
    • prop를 비교하는 방식 → shallow compare 해서 판단함 , 참조형을 비교하는 방식임
      • 이 기본적인 비교로직을 사용하지 않을 경우 변화를 판단하는 함수를 인자로 받을 수 있도록 설정
      • 의도한 결과가 나올 수 있게 비교함수를 작성할 땐 주의해야 한다!

자바스크립트 데이터 타입

react.memo는 기본적으로 props 객체간을 비교하는 방식을 통해서 동작합니다.

기본형 타입 vs 참조형 타입의 차이에 대해서 명확히 알고 있어야 한다.

  • 기본형
    • 원시형 타입
    • js에서 지원하는 원시적으로 기본적인 데이터 타입
    • 데이터 스스로 온전히 존재할 수 있는 형태
  • 참조형
    • 객체형 타입
    • 원시형 타입을 제외한 Object가 참조형 타입이라고 할 수 있음
      • array, funtion, 등등…
    • 다른 데이터를 모아서 만들어진 타입

불변성

불변성이란 값이 변하지 않는 것을 의미 → 기본적으로 원시형 타입은 모두 불변성

참조형 타입은 가변함 . → 객체는 여러 타입들을 모아서 만들어진 형태임, 객체 안의 내용물들은 언제든지, 어떤 형태로든 변경할 수 있음.

js에서는 기본적으로 비교연산자를 수행할 때 해당 데이터의 메모리 주소를 통해 일치 여부를 판단한다.

객체의 경우에는 내용물과 관계 없이 해당 객체를 가리키는 메모리 주소는 동일하기에 실질적으로 내용이 변했는지를 판단하기는 어렵다.

또한 두 객체의 내용이 같더라도 메모리 주소가 다르기에 두객체는 동일하지 않다는 결과가 나오게 된다.

그럼 어떻게 객체를 비교하나?

  • 내용물의 비교는 → 순회가 답이다.
  • 근데 객체의 중첩에 따라 복잡도가 천차 만별인데, 이 깊이를 정하는 방법이 shallow compare vs deep compare

리액트에서는 기본적으로 얕은 비교를 하기 때문에 모든 값들을 불변성으로 맞춰져야 한다?

  • 불변성 그래서 prev → prev +1 이 불변성을 지키는 거구나, 렌더링 때 값을 캡쳐 하고 있으니까

memo의 잘못된 활용

React.memo는 기본적으로 props의 변화를 이전 props와 새로운 props를 shallow compare 해서 판단함

  • props 객체 자체를 비교하는 것은 없다.
  • props는 객체 형태로 표현된, props 객체는 매 렌더링 마다 새롭게 생성됨 → 주소가 항상 바뀜 → 무조건 리렌더링
  • Object.is(===) 를 이용한 props객체안의 각 property들 비교
const areEqual = (prevProps, nextProps) => {
	if(prevProps.name !== nextProps.name) return false;
	if(prevProps.hello !== nextProps.hello) return false;

	return true;
}

Memoizaion

memoization은 특정한 값을 저장해뒀다가, 이후에 해당 값이 필요할 때 새롭게 계산해서 사용하는게 아니라 저장해둔 값을 활용하는 테크닉을 얘기함.

  • 함수 컴포넌트는 근본적으로 함수임 → 리엑트는 매 렌더링 마다 함수 컴포넌트를 다시 호출함
  • 함수는 기본적으로 이전 호출과 새로운 호출간에 값을 공유할 수 없음.

useMemo

useMemo는 리액트에서 값을 memoization 할 수 있도록 해주는 함수입니다.

// useMemo(callbackFunction, deps]

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo는 두가지 인자를 받습니다.

  • 첫번째 인자는 콜백함수 → 함수에서 리턴하는 값이 메모됩니다.
  • 두번째 인자는 의존성 배열
  • 메모이제이션을 할 때 주의해야 할 점은 만약 새로운 값을 만들어서 사용해야 하는 상황임에도 불구하고 이전의 결과를 그대로 활용해버리면 버그가 발생할 수 있다는 점]

useCallback

  • useCallback은 useMemo를 조금 더 편리하게 사용할 수 있도록 만든 버전
  • 함수를 useMemo를 이용해서 사용하게 되면 콜백함수에서 또 다른 함수를 리턴하는 형태가 된다. → 동작상에는 아무런 이상이 없지만 코드스타일에 따라 보기 불편해질 수 있음
const memorizedFunction = useMemo(() => () => console.log("Hello World"), []);

const memorizedFunction = useCallback(() => console.log("Hello World"), []);
  • setState함수는 react에서 기본적으로 메모이제이션 되어있다. → useCallback deps에 넣어주지 않아도 된다.

언제 memoizaion을 해야할까?

새로운 값을 만드는 것과 어딘가에 이전의 값을 저장해두고 메모이제이션 함수를 호출하고 의존성을 비교해서 가져올지 말지 여부를 판단하는 것 중 어떤 것이 비용이 더 적게 들까?

메모이제이션은 무조건 사용하는것이 좋은게 아니라, 필요성을 분석하고 필요하다고 판단되는 순간에만 사용해야함.

  1. 새로운 값을 만드는 연산이 복잡하다.
  2. 함수 컴포넌트의 이전 호출과, 다음 호출 간 사용하는 값의 동일성을 보장하고 싶다.

1번의 경우에는 만약 10000개의 요소를 가진 배열이 있다고 생각하면 이 배열을 매번 생성하는 것 보다는 메모해서 활용하는 것이 효율적

2번의 경우에는 함수 컴포넌트의 호출 간 값들의 동일성을 보장하기 위해서 사용한다. 왜 동일성을 보장해야할까? 바로 React.memo 와 연동해서 사용하기 위해서 (useEffect에서도 사용될 수 있음)

props로 전달되는 객체의 동일성이 보장되지 않아 실제 객체의 내용은 똑같아도 shallow compare를 통해서 다른 객체라고 판단되어서 매번 리렌더링이 실행되는 상황을 확인가능함 이런 상황에서 객체의 동일성을 보장하기 위해 메모이제이션을 활용할 수 있음!

useEffect

의존성 배열

  • useEffect에 두번째 인자로 넘기는 배열
  • 두번째 인자를 넘기지 않으면 Effect는 매번 실행되고, 빈 배열을 넘긴다면 컴포넌트의 첫번째 렌더링 이후에만 실행

⚠️ 이정도 수준까지만 이해하고 그 동안 useEffect를 사용하고 있었다면, 애플리케이션에서 버그가 발생할 확률이 굉장히 높다.

useEffect(effect, 의존성)

effect는 함수의 형태로 표현, 의존성은 여러 의존성들을 한번에 전달하기 위해 배열의 형태로 표현됨

의존성이란?

  1. A라는 요소가 온전히 동작하기 위해 B,C,D등 다른 요소들을 필요로 할 때, A는 B,C,D에 의존하고 있다고 표현
  2. Effect 함수가 의존하고 있는 요소들의 모음

“의존하고 있다” 라는 말이 어색하고 잘 이해가 안될 수도 있습니다. 이를 쉽게 풀어서 설명하자면 단순히 그냥 effect 함수가 사용하고 있는 외부의 값들이 의존성

function Component(){
	const [count, setCount] = useState(0);
	
	const effect = () => {
		document.title = `you clikced ${count} times`
	};

	useEffect(effect, [count]];
}

위에 코드에서 effetc함수에서 count의 값을 사용하는 중이다. count ⇒ 의존성이 있는 것

useEffect 의존성 배열을 잘 설정하는 법

useEffect에서 버그가 발생하지 않게 의존성 배열을 잘 설정하는 방법은 아래의 원칙만 지켜주면 됩니다.

  • “모든 의존성을 빼먹지 말고 의존성 배열에 명시해라”

여기에 덧붙여서 아래의 원칙을 추가해주면 좋습니다.

  • “가능하다면 의존성을 적게 만들어라”

그런데 간단한 일반 변수, state, props의 경우에는 의존성을 빼먹지 않고 의존성 배열에 명시하기가 쉽습니다.

하지만, 함수 컴포넌트의 내부에서 선언한 Object, Function의 경우에는 함수 컴포넌트의 매 호출마다 새로운 객체, 함수가 선언되고 참조형 데이터 타입의 특징으로 인해 객체 내부의 요소들이 동일하더라도 새롭게 생성된 객체와 이전 객체를 비교하면 서로 다른 객체라고 판단되게 됩니다.

그래서 아래 코드는 무한 루프를 반복하게 됨.

function Component(){
	const [count, setCount] = useState(0);

	const increaseCount = () => {
		setCount(prev => prev + 1);
	}

	useEffect(increaseCount, [increaseCount]];
}

위의 문제를 해결하기 위해 여러가지 방안을 시도해 볼 수 있음

  1. 의존성 제거하기 ⇒ 함수를 effect안에 선언
function Component(){
	const [count, setCount] = useState(0);

	useEffect(() => {
		const increaseCount = () => {
			setCount(prev => prev + 1);
		};

		increaseCount();
	}, []];
}
  1. 함수를 컴포넌트 바깥으로 이동시키기
// good
function Component() {

	useEffect(() => {
		const token = getUserAuth();
		// login....
	}, [getUserAuth]];

};

const getUserAuth = () => {
	localStorage.getItem("ACCESS_TOKEN");
};
  1. 메모이제이션 : 최후의 보루 느낌
function Component(){
	const [count, setCount] = useState(0);

	const increaseCount = useCallback(() => {
		setCount(prev => prev + 1);
	}, []);

	useEffect(() => {
		// do something 1
		increaseCount();
	}, [increaseCount]];

	useEffect(() => {
		// do something 2
		increaseCount();
	}, [increaseCount]];
}

 

같이 읽으면 좋은글: https://www.rinae.dev/posts/a-complete-guide-to-useeffect-ko