2.3.1 클래스 컴포넌트
🔖오래된 코드의 유지보수나 라이브러리 사용을 효율적으로 하기 위해선 역사의 뒤안길로 사라진 클래스형 컴포넌트를 알 필요가 있다.
기본적으로 클래스 컴포넌트는 클래스 선언과
extends
키워드를 통한 컴포넌트 상속으로 이루어진다.클래스 컴포넌트를 만들기 위해
extends
하는 컴포넌트이고,shouldComponentUpdate
컴포넌트를 다루는데서 둘의 차이가 생긴다.React.Component
React.PureComponent
// 사용 예시 import React from "react"; class Sample extends React.Component { render() { return <h2>Hi~</h2>; } }
컴포넌트를 만들 때 사용하는
props
,state
, 메서드의 정의import React from "react"; interface SampleProps { required?: boolean; text: string; } interface SampleState { count: number; isLimited?: boolean; } class SampleComponent extends React.Component<SampleProps, SampleState> { private constructor(props: SampleProps) { super(props); this.state = { count: 0, isLimited: false, }; } private handleClick = () => { const newValue = this.state.count + 1; this.setState({ count: newValue, isLimited: newValue >= 10 }); }; public render() { const { props: { required, text }, state: { count, isLimited }, } = this; return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={this.handleClick}>Click me</button> </div> ); } } export default SampleComponent;
constructor()
: 컴포넌트 내부에 존재할 시 컴포넌트 초기화 시점에 호출,state
를 초기화하며,super()
를 통해 상속받은 상위 컴포넌트에 접근props
: 컴포넌트에 특정 속성을 전달하는 용도, 선언한 형태에 맞게 사용state
: 컴포넌트 내부에서 관리하는 값, 항상 객체여야 하고 값의 변화 감지 시 리렌더링메서드 : 렌더링 함수 내부에서 사용, DOM이벤트와 함께 사용하며 방식은 3가지로 분류
constructor
에서this
바인딩 하기
type Props = Record<string, never>; interface State { count: number; } class SampleComponent extends Component<Props, state> { // 생략 // handleClick의 this를 현재 클래스로 바인딩 private constructor(props: Props) { this.handleClick = this.handleClick(this); } // 사용 public render() { // 생략 return <button onClick={this.handleClick}>증가</button>; } }
화살표 함수 사용 : 작성 시점에
this
가 상위 스코프로 결정되는 화살표 함수 사용(바인딩 X)렌더링 함수 내부에서 함수를 바로 만들어 사용 : 렌더링이 일어날 때마다 함수가 새로 작성되므로 최적화가 어렵다 -> 비추🚫
render() { // 생략 return <button onClick={() => this.handleClick()}>증가</button> }
🔖 클래스 컴포넌트의 생명주기 메서드
🏷️ 클래스 컴포넌트를 공부할 땐 항상 생명 주기와 생명 주기의 각 시점들을 염두에 두자!
마운드(mount) : 컴포넌트가 마운팅(생성)되는 시점
업데이트(update) : 생성된 컴포넌트의 내용이 변경되는 시점
언마운트(unmount) : 컴포넌트가 더 이상 존재하지 않는 시점
render()
: 클래스 컴포넌트의 유일 필수 값, UI 렌더링 목적render()
는 항상 순수해야 하고 부수 효과(side effect)가 없어야한다.항상 간결하고 깔끔하게 작성해야 하고 state를 직접 호출해선 안된다.
componentDidMount()
: 마운트 시 처음으로 실행되는 메서드this.setState()
를 통해state
값을 변경하는 것이 가능하다.State
가 변경되면 브라우저가 UI를 업데이트하기 전에 실행하여 사용자가 렌더링 하는 것을 눈치챌 수 없게 한다.만능이 아니므로 굳이 이 메서드가 아니더라도 작업할 수 있는 경우엔 사용하지 않는 것이 좋다. (API 호출 후 업데이트, DOM 의존성 작업 등에만 사용)
compoenetDidUpdate()
: 컴포넌트 업데이트가 일어난 이후 실행하며 상태값이나props
의 변화에 따라 DOM을 업데이트 하는데 쓰인다.this.setState
사용 시 조건을 명확히 걸지 않는다면 계속해서 호출될 수 있다. (성능 문제 야기)
componentWillUnmount()
: 컴포넌트가 언마운트 되거나 사용되지 않을 때 호출하며 클린업 함수 호출에 최적화 되어있다.this.setState()
호출이 불가하다.
shouldComponentUpdate()
:state
나props
의 변경에 의한 리렌더링을 방지하고 싶을 때 사용하며 기본적으로 특정한 최적화 상황에서만 사용해야 한다.// props의 title이나 state의 input 값이 다를 경우에만 true (리렌더링) shouldComponentUpdate(nextProps: Props, nextState: State) { return this.props.title !== nextProps.title || this.state.input !== nextState.input }
🏷️ 위에서 언급한 Component
와 pureComponent
의 차이가 바로 shouldComponentUpdate()
를 어떻게 다루느냐에 있다.
기본적으로
shouldComponentUpdate()
의 리턴값은true
이고, 다시 말해 이는state
나props
의 변경 여부를 신경쓰지 않고 실행되기만 하면 리렌더링 한다는 의미이다.pureComponent
도 마찬가지로shouldComponentUpdate()
가 구현되어 있지만,props
와state
를 얕은 비교를 통해 비교하여 변경된 것이 없다면false
를 반환하여 리렌더링을 방지한다.
❗pureComponent()
는 얕은 비교만 수행하므로 무분별하게 사용 시 제대로 동작하지 않을 수 있다.
static getDerivedStateFromProps()
: 다음에 올props
를 바탕으로 현재props
를 변경할 때 사용한다.static
으로 선언되어this
에 접근할 수 없다.반환하는 객체의 내용은 모두
state
로 들어간다.null
반환 시 아무 일도 일어나지 않는다.
getSnapShotBeforeUpdate()
: 이름에서도 알 수 있듯이 새로 업데이트 될 클래스가 렌더링 되기 전 이전 클래스의 값을 기억해놨다가 새로운 클래스로 전송한다.- 업데이트 전에 윈도우 크기를 조절하거나 스크롤 위치 조절 등에 사용한다.
❗다음 메서드들은 기존과 다르게 Error 발생 상황에서 사용하는 메서드이다.
getDerivedStateFromError()
: 자식 컴포넌트에서 에러가 발생할 시 사용되는 메서드이다.static
한 컴포넌트로 하위 컴포넌트에서 발생한 에러를 인수로 받는다.반드시
state
값을 반환해야 하고, 에러를 받아 적절하게 렌더링하는 용도로 사용한다.에러에 따른 상태
state
를 반환하는 작업만을 진행해야 하며render
에서 호출되는 메서드이기 때문에 불필요한 부수 효과를 추가할 필요는 없다. (console.error() 사용 등)
componentDidCatch()
: 자식 컴포넌트에서 에러가 발생했을 때 실행되며,getDerivedStateFromError()
가 에러를 잡고 state를 반환한 후에 실행된다.부수 효과 사용이 가능하다.
커밋 단계에서 실행되어 에러 발생 시 에러를 바탕으로 정보를 로깅하는 용도로도 사용 가능하다.
에러 경계 컴포넌트인
ErrorBoundary
를 여러 개 만들어서 사용 가능하여 컴포넌트 별로 에러 상황에 대처할 수 있다.에러 발생 영역만 따로 처리하여 에러가 앱 전체로 퍼지는 걸 막을 수 있다.
😅 클래스 컴포넌트의 한계
데이터의 흐름을 추적하기 어렵다.
- 서로 다른 여러 메서드에서
state
의 업데이트가 일어날 수 있어 이를 추적하여 렌더링이 일어나는 과정을 판단하기 어렵다.
- 서로 다른 여러 메서드에서
앱 내부 로직의 재사용이 어렵다.
- 재사용 시 컴포넌트를 또 다른 고차 컴포넌트에 넘겨야 하는데, 이런 식의 작업을 반복하면 래퍼 지옥에 빠질 수 있다.
기능이 많을 수록 컴포넌트의 크기가 커진다.
- 내부의 로직과 컴포넌트가 처리하는 데이터가 커질 수록 컴포넌트 자체의 크기가 너무 커진다.
클래스가 어렵다..
- 클래스보다 함수에 익숙한 개발자가 많고, JS 환경 자체에서 클래스를 사용하는 것이 쉽지 않다.
코드 크기를 최적화 하기 어렵다.
- 사용하지 않는 메서드도 모두 정의해야 하기 때문에 최종적으로 번들링 시 쓸데 없는 비용이 추가된다.
핫 리로딩을 하는데 상대적으로 불리하다.
코드에 변경사항이 발생했을 때 리렌더링 없이도 변경된 코드만 업데이트 하는 기법이다.
클래스 컴포넌트 사용 시에는
instance
내부의render
값이 수정되어 코드가 초기화 된다.
2.3.2 함수 컴포넌트
🔖 단순히 무상태 컴포넌트 구현을 위해 사용했으나, Hook의 출현 이후로 각광받고 있다.
- 클래스 컴포넌트와 다르게 훨씬 더 간결하게
state
와props
를 다룰 수 있다.
2.3.3 함수 컴포넌트 vs 클래스 컴포넌트
🔖 클래스와 함수 컴포넌트 사이에는 어떤 차이가 있고, 왜 클래스 컴포넌트는 도태되어갈까?
생명주기 메서드의 부재
함수 컴포넌트에는 생명주기 메서드가 존재하지 않는다. (React.Component 클래스를 상속받아 메서드를 사용하기 때문)
비슷한 훅으로
useEffect
가 있지만, 이는 생명주기를 흉내내어 동기적으로state
를 활용해 부수 효과를 만들 뿐, 같은 개념이 아니다.
함수 컴포넌트의 렌더링 값 고정
// 함수형 컴포넌트 function funcComponent(props) { const showMessage = () => { alert(`hello, ${props.user}`); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return <button onClick={handleClick}>클릭</button>; } // 클래스형 컴포넌트 class classComponent extends React.Component { private showMessage = () => { alert(`hello, ${this.props.user}`); } private handleClick = () => { setTimeout(this.showMessage, 3000); } return <button onClick={this.handleClick}>클릭</button>+ }
위 예제는 동일한 결과를 가질 것 같지만, 차이가 존재한다.
클래스는
props
의 값을 항상this
로부터 가져오기 때문에,props
는 불변이나this
가 변경 가능하여setTimeout
에 의한 3초간props
가 변경된다면 변경된 값을 출력하게 된다.결국 함수 컴포넌트가 짱이다..👍
클래스 컴포넌트를 공부해야 할까?
- 결론은 현재 널리 사용하는 함수 컴포넌트를 중점적으로 확실하게 공부하되, 클래스 컴포넌트가 아예 없어질 전망은 없으므로, 다양한 상황에 대비해서 조금씩은 공부하는 것이 좋을 듯 하다.
링크 : 리액트의 클래스형 컴포넌트
2.3.4 책 정리 + 주관적인 정리
🔖 책 정리
숙련된 리액트 개발자가 되기 위해선 어떤 고민들을 통해 리액트가 발전해 왔는지를 알아야 할 필요가 있다.
함수 컴포넌트를 먼저 익혀 숙달되면 클래스를 익혀 리액트를 완전히 정복해보자
🏷️ 주관적인 정리
예전에 프론트를 하겠다고 맘먹기 전에 백을 위해 자바 공부를 했었는데, 그 때 자주 사용하던 클래스의 개념이 나와 반가웠다.
리액트의 전반적인 흐름과 확실한 숙달을 위해서 현재 사용하는 함수 컴포넌트의 윗 세대인 클래스 또한 어느정도 공부하고 관련 지식을 알고 있으면 좋을 것 같다.
반갑긴 하지만 리액트에선 별로 사용하고 싶지 않다..
2.4 렌더링은 왜 일어나는가?
✨ 리액트의 '렌더링'이란 브라우저의 렌더링에 필요한 DOM 트리를 만드는 작업을 의미하며, 개발자라면 렌더링의 과정을 공부하고 이를 효율적으로 운용해야 한다.
2.4.1 리액트의 렌더링이란?
🔖 리액트 앱 트리 안에 있는 모든 컴포넌트들이 자신들의 props
와 state
를 기반으로 UI를 구성하고 DOM 결과를 브라우저에 제공하는 일련의 과정을 의미한다.
2.4.2 리액트의 렌더링이 일어나는 이유
🔖 렌더링 발생 시나리오
최초 렌더링 : 사용자가 처음 접속했을 때를 위해 최초 렌더링 시행
리렌더링 : 최초 이후 발생하는 모든 렌더링
- 클래스 컴포넌트의
setState
,forceUpdate
가 실행되는 경우 - 함수 컴포넌트의
useState
의 요소인setter
,useReducer
의 요소인dispatch
가 실행되는 경우 - 컴포넌트의
key props
가 변경되는 경우
- 클래스 컴포넌트의
❗key props
는 하위 컴포넌트를 선언할 때 사용하며, 동일 요소를 식별하기 위해 사용한다.(ex. 배열을 통해 동일한 요소들을 렌더링하는 경우)
- 앞서 보았던 파이버가 요소를 판별하는 것을 생각하면
key props
를 사용하는 이유를 유추할 수 있다.
✅ 결론적으로 리액트 상태관리와 관련된 라이브러리들이 리렌더링을 발생시키는 것이 아닌, 내부에서 useState
를 사용하여 리렌더링을 발생시키는 것이다.
2.4.3 리액트의 리렌더링 프로세스
🔖 리액트의 렌더링 과정을 알아보자
렌더링 시작 시 리액트는 루트부터 하위까지 업데이트가 필요한 부분을 찾는다.
업데이트 필요 부분을 찾으면 클래스의 경우
render()
실행, 함수의 경우 함수 자체를 호출한다.이런 과정을 통해 가상 DOM과 비교한 렌더링 결과물을 실제 DOM에 반영한다.
이는 렌더와 커밋이라는 두 단계로 분리되어 실행된다.
2.4.4 렌더와 커밋
🏷️ 렌더
컴포넌트를 렌더링하고 변경사항을 계산하는 모든 작업
컴포넌트를 실행해 가상 DOM과 비교하여 업데이트할 항목을 체크하는 모든 과정을 의미한다.
비교하는 것은 크게
type
,props
,key
이다.
🏷️ 커밋
렌더 단계을 통해 도출한 모든 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정
여기까지 완료해야 브라우저의 렌더링 발생
업데이트된 항목에 리액트의 내부 참조를 업데이트하여 연결하고, 클래스의 경우
componentDidMount()
,commponentDidUpdate()
를 실행하고, 함수의 경우useLayoutEffect()
훅을 실행한다.
❗여기서 알 수 있는 사실은 꼭 렌더 단계가 실행된다고 해서 커밋 단계가 실행되진 않는다는 것이다.
변경 사항 계산 시 변경 사항이 없다면, 커밋 단계가 생략될 수 있다.
이는 브라우저의 DOM 업데이트가 발생되지 않는 것으로 이어진다.
✅ 위 두 과정으로 인해 리액트의 렌더링은 항상 동기식으로 이루어지며, 이는 불필요한 렌더링이 발생할 경우 성능 저하로 이어질 수 있음을 보여준다.
2.4.5 일반적인 렌더링 시나리오 살펴보기
🔖 예시를 통해 렌더링 시나리오 살펴보기
import React, { useState } from "react";
import ReactDOM from "react-dom";
const B = memo(() => {
return <div>안녕~</div>;
});
function App() {
// useState를 사용하여 count 상태를 정의
const [count, setCount] = useState(0);
// 버튼 클릭 시 호출되는 함수
const handleClick = () => {
setCount(count + 1);
};
// 컴포넌트의 JSX 반환
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={handleClick}>Increment</button>
<B />
</div>
);
}
기본적으로 컴포넌트 렌더링은 별도의 조치가 없다면 해당 컴포넌트의 모든 하위에 영향을 미친다.
React.memo
를 사용해 변경되지 않는 컴포넌트의 리렌더링을 방지할 수 있다.
2.4.6 책 정리 + 주관적인 정리
🔖 책 정리
개발자는 항상 리액트의 눈에 보이지 않는 렌더링을 인지하고 다룰 수 있어야 한다.
리액트의 렌더링 시나리오를 정확히 이해하여 불필요한 리렌더링을 방지하고 성능 좋은 리액트 앱을 만들자!
🏷️ 주관적인 정리
UI가 변경되는 것이 곧 리액트의 리렌더링이라고 생각해왔는데, 눈에 보이지 않는 렌더링도 존재한다니 가슴이 답답해진다.
하지만 이를 잘 인지하고 공부하여 이해하는 것이 리액트의 근본을 파는 것이고 곧 실력있는 개발자가 되는 길이므로 열심히 공부해야 할 듯 하다.
2.5 컴포넌트와 함수의 무거운 연산을 기억해 두는 메모이제이션
✨ React.memo
, useCallback
, useMemo
를 통해 현명하고 효율적으로 메모이제이션 하는 방법을 알아보자
2.5.1 주장1 : 섣부른 최적화는 독이다. 꼭 필요한 곳에만 메모이제이션을 추가하자
🔖 메모이제이션 또한 비용이 드는 엄연한 작업이므로 항상 신중하게 사용해야 한다.
function sum(a, b) {
return a + b;
}
위 예제 같은 경우 메모이제이션과 매번 계산, JS에 맡기는 것들 중 뭐가 더 나을까?
극단적이긴 하나 엄연히 메모이제이션 또한 메모리를 사용해 기존 값을 가져오는 것과 이를 비교하는 판단에 드는 비용이 있으므로, 섣부르게 사용하는 것을 방지해야 한다.
개발자에게 메모이제이션 할 선택권을 쥐어준 것부터 알아서 유도리 있게 사용하라는 방증이다. 😠
2.5.2 주장 2 : 렌더링 과정의 비용은 비싸다. 모조리 메모이제이션 해버리자
🔖 일반적으로 컴포넌트들은 비싼 연산이 포함되고 자식 컴포넌트가 많이 딸려있을 확률이 높다.
이런 전제를 깔고 가는 경우 메모이제이션 하는 것이 확실히 도움이 될 것이다.
memo
를 컴포넌트의 사용에 따라 잘 살펴보고 적용memo
를 그냥 다 박아버리기
❗잘못된 memo
로 지불해야 하는 비용이 props
에 대한 얕은 비교가 발생 시 지불하는 비용이다.
리액트는 이전 결과물을 다음 결과와 비교하기 위해 저장해야 하고, 이를 비교하면서 발생하는 비용이
memo
로 지불하는 비용이다.하지만
memo
를 사용하지 않을 시 발생할 잠재적 비용이 훨씬 크다.렌더링 시 발생 비용
컴포넌트 내부 복잡한 로직의 재실행
자식에서 위 두 과정이 반복
리액트의 재조정 과정
🔖 useCallback
과 useMemo
의존성 배열 비교 및 필요에 따른 값 재계산 과정과 메모이제이션을 통한 과정에서 발생하는 비용을 항상 고민해야 한다.
// 함수 메모이제이션 예제 function useMath(number) { const [double, setDouble] = useState(0); const [tripple, setTripple] = useState(0); useEffect(() => { setDouble(number * 2); setTripple(number * 3); }, [number]); return { double, tripple }; } function App() { const [counter, setCounter] = useState(0); const value = useMath(10); useEffect(() => { console.log("hi"); }, [value]); const handleClick = () => { setCounter((prev) => prev + 1); }; return ( <> <h1>{counter}</h1> <button onClick={handleClick}>클릭</button> </> ); }
위 예제의 경우, 실제로 변하는 값이 없음에도 불구하고
console.log('hi')
구문이 실행된다.App
컴포넌트가 호출되며 계속해서useMath
훅을 호출하고 참조가 변경되기 때문이다.따라서
useMath
의 리턴값을useMemo()
를 통해 감싸면, 비교를 수행하며 변하지 않은 값으로 인한 렌더링을 방지할 수 있다.
책 정리 및 주관적인 정리
🔖 책 정리
아직 리액트에 대한 이해가 부족하다면, 섣부른 메모이제이션을 지양하며 어느 지점에서 이를 사용하면 좋을지 고민하는 형식으로 메모이제이션을 차근차근 적용해야 한다.
하지만 현업에서 이미 종사하고 있거나 성능에 대해 깊게 연구할 여유가 없다면 일단 다 적용해보는 것이 좋다. 섣부른 메모이제이션의 비용이 다른 모든 비용보다 싼 경우가 많다.
🏷️ 주관적인 정리
세가지의 메모이제이션용 훅들에 대해 알고는 있었지만 사용해본 경험이 많지 않았다. 이번에 공부하면서 정확한 사용 용도와 사용법, 그리고 사용에 대한 고민까지 다양하게 접할 수 있어 좋았다.
현재 스프린트 미션이나 기초 프로젝트 결과물에도 적용해보면서 사용감을 익히는 것이 좋을 듯 하다. 책에서 막 갈기라고 했다고 진짜 아무데나 갈기지 말자.
링크 : 전부 메모이제이션 할까요?
'Front-End Study > 모던 리액트 딥다이브 스터디' 카테고리의 다른 글
모던 리액트 딥다이브 - 8회차 [4-1, 4-2] (0) | 2024.06.25 |
---|---|
모던 리액트 딥다이브 - 7회차 [3-1, 3-2, 3-3] (2) | 2024.06.19 |
모던 리액트 딥다이브 - 5회차 [2-1, 2-2] (1) | 2024.06.10 |
모던 리액트 딥다이브 - 4회차 [1-6, 1-7] (1) | 2024.06.08 |
모던 리액트 딥다이브 - 3회차 [1-3, 1-4, 1-5] (1) | 2024.06.03 |