우파루파의 개발 기록

[React.js] Shallow compare와 Class component에서의 최적화 (22.04.13. 내용 수정 및 추가) 본문

development/React

[React.js] Shallow compare와 Class component에서의 최적화 (22.04.13. 내용 수정 및 추가)

upa-r-upa 2021. 6. 17. 22:44

React.js

들어가며

안녕하세요. 이번 포스팅에서는 Shallow compare Class component 에서의 최적화에 대해 말씀드리려고 합니다.

메모리에 대한 이해가 있으시면 좀 더 쉽게 이해하실 수 있습니다. 물론 없어도 무관 합니다.

최적화란 무엇일까요?

최적화란 말 그대로 성능을 개선하여 가장 적은 비용으로 가장 좋은 퍼포먼스를 낼 수 있도록 하는 것입니다.

들어는 봤는데, 너무 어렵지 않나요?

'Webpack을 이용하여 build한 파일을 경량화하고, 의존성을 파악하여 코드 스플리팅을 진행하고.. '

 

맞습니다. 최적화는 깊이 파고들면 어려운 방식들이 가득합니다. 

하지만 위와 같이 어려운 작업은 하나씩 천천히 진행하면 됩니다.

너무 어려운 작업 말고, 작업 중에서도 가장 쉽게 접근할 수 있는 방법에 대해 설명해 드릴 테니까요! 잘 따라와 주시기 바랍니다.

Class 컴포넌트 최적화 방법엔 어떤 것이 있을까요?

Class 컴포넌트의 최적화 방식은 대표적으로 2가지를 말씀 드릴 수 있을 것 같습니다.

바로 shouldComponentUpdate method, PureComponent 입니다! 예상하셨듯이 오늘 포스팅할 주요 내용도 위 두 가지입니다.

 

두 가지 항목을 알아보기 전, 먼저 JS의 자료형 별 특징에 대해서 알아야 합니다.

참조 타입과 원시 데이터 형식의 특징

JS의 데이터 타입에는 string, number, boolean, array 등 여러 가지 형식이 있습니다. 

참조 타입원시 데이터 형식의 메모리 저장 방식은 메모리에 대한 이해가 조금 필요한데요, 지금은 간단하게 알려 드리겠습니다.

 

형식별로 A 변수를 선언 후 B 변수로 복사할 때 동작하는 방법을 보여드리며 설명드리겠습니다. 

원시 데이터 형식의 경우 (Immutable)

let a = 10;
let b = a;

console.log(a, b);
// 출력: 10, 10

a = 5;

console.log(a, b);
// 출력: 5, 10
  1. a 변수에 10을 할당합니다. 
    - 단순하게 리터럴만을 저장해두는 저장소와 변수의 선언부를 저장해두는 저장소가 따로 있다고 가정합니다.
    - 리터럴 저장소에서 10이라는 리터럴 값을 찾고, 만약 값이 있다면 a라는 변수 선언부 저장소에 해당 값의 메모리 저장소 주소를 쌍으로 묶어 저장합니다. 없다면 리터럴 저장소에 10을 추가하고, 같은 과정을 진행합니다.
  2. b 변수에 a 값을 [복사] 합니다. 
    - a 변수가 바라보고 있던 변수 선언부 저장소 > 리터럴 값 저장소 주소를 그대로 복제하여 새로운 선언부 저장소의 메모리에 저장합니다. 이제부터 a와 b는 변수 선언부 저장소의 위치는 다르지만, 리터럴 값 저장소의 위치는 같은 내용을 바라보게 됩니다.
    - 이렇게 되면 같은 10이라는 값을 여러 메모리 주소에 저장하지 않게 되어 효율적으로 값을 관리할 수 있습니다.
  3. a 데이터의 값을 5로 변경합니다. 
    - 해당 과정에선 a 변수가 바라보고 있던 값을 직접 수정하지 않습니다. 1번 과정을 비슷하게 반복합니다. 
    5라는 값을 리터럴 저장소에서 들고 있는지 확인한 이후, 만약 값이 있다면 ... (1번 뒷부분 반복) 하게 됩니다.
  4. 이렇게 되면 값을 아무리 변경하더라도, 값을 수정하는 것이 아닌 리터럴 저장소에서 값을 찾아 그 주소를 연결시켜주기 때문에 각 데이터에 영향이 없습니다

참조 타입의 경우 (Mutable)

let a = { value: 10 };
let b = a;

console.log(a, b);
// 출력: { value: 10 }, { value: 10 }

a.value = 5;

console.log(a, b);
// 출력: { value: 5 }, { value: 5 }
  1. a 변수에 객체를 할당합니다.
    - 단순하게 리터럴만을 저장해두는 저장소와 변수의 선언부를 저장해두는 저장소가 따로 있다고 가정합니다.
    - a 변수의 { value: 10 } 값은 선언 시 리터럴 저장소에 새로이 추가됩니다. 그리고 리터럴 저장소 메모리 주소를 변수 선언부 저장소 영역에 저장하게 되고, a 변수는 이러한 리터럴 저장소의 특정 위치를 바라보게 됩니다. 
  2. b 변수에 a 값을 할당합니다. 
    - a 변수가 바라보고 있던 리터럴 저장소의 값을 
  3. a  변수의 value의 값을 5로 변경합니다. 
    - a 변수의 'value' 값을 변경하게 됩니다.
    - 같은 값을 바라보고 있던 b도 변경된 값을 바라보게 됩니다.
  4. 참조 형식의 경우, `b = a` 와 같은 형식으로 복사하게 되면(이걸 얕은 복사라고 합니다.) 결국 같은 메모리 주소를 바라보게 되기 때문에 값을 수정했을 때 서로에게 영향을 끼칩니다. 그

이와 같은 특징 때문에 참조 형식의 경우 내부의 깊은 값이 변경된다고 해도, 객체 자체의 메모리 주소는 같기 때문에 변경되지 않았다고 판단하는 불상사가 있을 수 있습니다.

shouldComponentUpdate method

shoudComponentUpdate 메서드는 React Component의 내장 메서드입니다.

 

컴포넌트가 리 렌더링 되기 전에 호출되는 메서드로, boolean 값을 return 합니다. 

메서드에서 return 되는 값을 기준으로 리 렌더링의 여부를 결정합니다.

 

return 되는 값이 참이라면 리 렌더링을 진행하고, 거짓이라면 리렌더링을 하지 않습니다.

 

컴포넌트의 리 렌더링은 PropsState가 변경되었을 때나, 부모 컴포넌트가 리 렌더링 될 때 발생합니다.

그 이유로 위 메서드는 기본적으로는 항상 true를 return 하기 때문입니다.

 

컴포넌트의 값이 유효하게 업데이트되지 않았는데도 불필요하게 리 렌더링이 일어난다면 당연히 성능상의 이슈가 발생할 수 있겠죠?

그러나 걱정 마세요! 이 메서드를 잘 사용하면 원하는 조건에서만 컴포넌트가 리렌더링 되도록 설정하여 최적화를 진행할 수 있어요. 

 

다음과 같은 코드로 구현할 수 있습니다.

아래 코드는 props의 [김다빈]이라는 값이 변경되는 경우만 rerendering 하도록 합니다

shouldComponentUpdate(nextProps, nextState) {
  // 파라미터로 next props와 next state가 들어옵니다.
  if (this.props.김다빈 !== nextProps.김다빈) {
  	return true;
  } else {
  	return false
  }
}

위 코드에선 props로 전달되는 김다빈이라는 값을 비교해서 실제로 변경됐을 때만 리 렌더링이 일어나도록 하고 있습니다.

 

그런데, 분명 잘 동작할 것이고 이걸 이용하면 최적화도 할 수 있는 게 맞는데.. 뭔가 귀찮습니다.

우리는 단순하게 컴포넌트에 전달되는 props나 state가 변경됐을 경우에만 리 렌더링 하고 싶은 건데, 그렇다면 propsstate들을 하나하나 if문으로 변경됐는지 처리해줘야 하는 걸까요?

 

아닙니다! 이걸 자동으로 처리해주는 좋은 친구가 있습니다.

바로, React.PureComponent입니다.

 

React.PureComponent

어디서 들어보신 분들도 계실 텐데요, 네 맞습니다. 이 친구가 바로 귀찮은 일을 어느 정도 해줄 수 있는 귀여운 친구입니다.

PureComponent는 에서 내부적으로 shoudComponentUpdate 메서드를 미리 구현해 PropsState가 변경이 됐는지를 얕은 비교를 통해 검증하여 리렌더링을 할 수 있게 해줍니다.

그 대신, 일반 Component 처럼 shoudComponentUpdate 메서드를 오버라이딩 할 수 없습니다.

여기서 언급되는 얕은 비교란 간단하게 말씀 드리면 다음과 같습니다.

 

얕은 비교란?

  • 원시 타입의 경우는 값 자체를 비교합니다.
  • 참조 타입의 경우는 메모리 주소를 비교합니다.

제가 왜 어느정도 대신 해 줄 수 있다 라는 부분을 강조했는지 눈치 채신 분이 계실까요? 

혹시 맞추셨다면 칭찬 드리고 싶습니다. 이유는,  얕은 비교를 진행하기 때문에 원시 타입의 데이터가 아니라면 값이 진짜 변경 됐는지를 정확하게 판단할 수 없게 됩니다. 

 

그러니 PureComponent를 사용하실 때는 업데이트 됐는지 검증 하고자 하는 값이 참조 타입의 값인 경우는 써도 의미가 없을 수 있습니다. 이 부분을 신경쓰지 않고 아무 컴포넌트에서나 PureComponent를 남용하게 되면, 제대로 업데이트가 되어야 하는 순간에 업데이트가 되지 않았다고 판단하여 오히려 나쁜 결과를 초래할 수 있습니다.

 

마치며

이번 포스팅에서는 Class 컴포넌트의 최적화와 이를 이해하는 데 필요한 내용들을 부수적으로 알아보았습니다.

아무래도 메모리에 대한 내용이 자주 나왔습니다. 곧 메모리에 대한 자세한 내용을 다루도록 노력 해보겠습니다.

글을 읽으시며 이해가 어려우셨던 부분이나 잘못된 부분이 있으면 코멘트로 남겨주세요. 감사합니다. 


+ 2022.04.13. 메모리 관련 내용을 좀 더 정확하게 수정하였습니다.