>[!tip]- > tip 블록 안의 내용은 책의 내용이 아닌 개인적인 궁금증으로 조사한 내용입니다. 함수형 컴포넌트는 리액트 0.14버전에서 소개된 꽤 역사가 깊은 컴포넌트 선언 방식이다. 이 때는 stateless functional component라고 해서 상태 없이 요소를 정적으로 렌더링하기 위해서 사용하는 방식이었다. 함수형 컴포넌트가 떠오르기 시작한 것은 리액트 16.8 버전에서 훅이 소개된 이후였다. 훅이 등장한 이후부터 함수형 컴포넌트에서 상태나 생명주기 메서드 비슷한 작업을 할 수 있게 되면서 선언할 때 보일러플레이트가 복잡한 클래스 컴포넌트보다 함수형 컴포넌트를 더 많이 쓰게 된 것이다. ## 클래스 컴포넌트 기본적인 클래스 컴포넌트의 구조 : ```javascript import React from 'react'; class SampleComponent extends React.Component { render() { return <h2>Sample Component</h2> } } ``` 클래스 컴포넌트는 React.Component나 React.PureComponent 중 하나를 확장해서 만들 수 있다. 둘의 차이는 [[shouldComponentUpdate]]에 있다. ```tsx interface SampleProps { required?: boolean; text: string; } interface SampleState { count: number; isLimited?: boolean; } // 1️⃣ 제네릭으로 props와 state를 순서대로 넣어준다. class SampleComponent extends React.Component<SampleProps, SampleState> { // 2️⃣ 생성자에서 props를 넘겨주고, state의 기본값을 설정한다. constructor(props: SampleProps) { super(props); this.state = { count: 0, isLimited: false, }; } // 3️⃣ render 안에서 쓰일 함수를 정의한다 private handleClick = () => { const newValue = this.state.count + 1; this.setState({ count: newValue, isLimited: newValue >= 10 }); }; // 4️⃣ render에서 컴포넌트가 렌더링 될 내용을 정의한다. public render() { const { props: { required, text }, state: { count, isLimited }, } = this; return ( <> <h2>Sample Component</h2> <div>{required ? '필수' : '선택'}</div> <div>{text}</div> <div>count: {count}</div> <button onClick={this.handleClick} disabled={isLimited}> 증가 </button> </> ); } } ``` 1. 컴포넌트 내부에 생성자 함수가 있다면 컴포넌트가 초기화되는 시점에 이 생성자 함수가 호출된다. 여기서 컴포넌트의 state를 초기화 할 수 있다. super로 부모 클래스의 생성자를 먼저 호출해서 필요한 상위 컴포넌트에 접근할 수 있다. 2. state는 클래스 컴포넌트 안에서 관리하는 값인데, 이 값은 항상 객체여야 한다. 3. 메서드 : 메서드를 일반 함수로 정의한 경우에는 this 바인딩에 신경써야 한다. 따로 바인딩하지 않는다면 일반 함수로 정의한 메서드 내부의 this는 전역 개체를 가리키게 된다. (strict mode에서는 undefined) > [!note] 생성자 없는 클래스 > 생성자 없이도 클래스 내부에 필드를 선언할 수 있는데, 이것은 ES2022에서 등장한 클래스 필드 덕분에 가능한 문법이다. 비교적 최신 문법이기 때문에 브라우저 환경이나 babel의 트랜스파일 플러그인을 신경써야 한다. > [!note] 일반 함수 this 바인딩 > ```tsx > // 빈 props type > type EmptyProps = Record<string, never>; > > interface SampleState { > count: number; > } > > class SampleComponent extends React.Component<EmptyProps, SampleState> { > private constructor(props: EmptyProps) { > super(props); > this.state = { count: 0 }; > // this.handleClick = this.handleClick.bind(this); > } > > private handleClick() { > // this 바인딩을 하지 않으면 this를 통해 setState에 접근할 수 없다. > // Cannot read properties of undefined (reading 'setState') > this.setState((prev) => ({ > count: prev.count + 1; > })); > } > > public render() { > const { > state: { count }, > } = this; > > return ( > <> > <div>{count}</div> > <button onClick={this.handleClick}>증가</button> > </> > ); > } > } > ``` ### 클래스 컴포넌트의 생명주기 메서드 생명주기 메서드 실행되는 시점은 크게 3가지로 나눌 수 있다. 1. 마운트 (mount) : 컴포넌트가 마운팅(생성)되는 시점 2. 업데이트 (update) : 이미 생성된 컴포넌트의 내용이 변경되는 시점 3. 언마운트 (unmount) : 컴포넌트가 더 이상 존재하지 않는 시점 #### `render()` `render`도 생명주기 메서드이다. 리액트 클래스 컴포넌트의 유일한 필수 값이다. 컴포넌트가 UI를 렌더링하기 위해서 쓰인다. 이 렌더링은 마운트와 업데이트 과정에서 일어난다. 주의할 점은 `render`는 항상 순수해야 하고, 부수 효과가 없어야 한다는 것이다. 즉 같은 입력값(props, state)에는 같은 UI를 반환해야 한다는 것이다. 이런 이유에서 `render` 안에서는 `this.setState`를 호출하면 안된다. 그래서 `render`는 가급적이면 최대한 간결하고 깔끔하게 작성하는게 좋다. (부수효과 발생 가능성을 낮추고 컴포넌트의 유지보수성을 높일 수 있다) #### `componentDidMount()` 클래스 컴포넌트가 마운트되고 준비된 이후에 실행되는 생명주기 메서드이다. 이 메서드에서는 `render`와는 다르게 `this.setState`를 호출해서 상태를 업데이트 할 수 있는데, 상태를 업데이트하면 그 즉시 다시 한번 렌더링을 시도한다. 이 작업은 브라우저 페인팅 전에 실행되어서 사용자가 리렌더링을 눈치챌 수 없게 할 수 있다. > [!tips] componentDidMount()의 실행과 결과 시점 정리 > 1. render 메서드 실행 -> React가 가상 DOM을 생성 > 2. React가 실제 DOM에 변경사항 반영 (commit 단계) > 3. 브라우저가 화면에 페인팅하기 직전에 componentDidMount 실행 > 4. componentDidMount 내부에서 setState를 호출하면 즉시 재렌더링 발생 > 이 재렌더링은 브라우저 페인팅 전에 완료되기 때문에 사용자는 중간 상태를 볼 수 없다. > [componentDidMount – React](https://ko.legacy.reactjs.org/docs/react-component.html#componentdidmount) `componentDidMount`는 브라우저 렌더링 전에 실행되는 메서드이므로 성능 이슈가 발생할 수 있다. 일반적으로 state 초기화는 생성자에서 하는 것이 좋다. `componentDidMount`에 해야만 하는 작업인지(예를 들면 API 호출 후 업데이트, DOM에 의존적인 작업(이벤트 리스너 추가 등)등)를 확인하고 사용하는 것이 좋다. #### `componentDidUpdate` `componentDidUpdate`는 컴포넌트 업데이트가 일어난 이후 바로 실행된다. 일반적으로는 state나 props의 변화에 따라 DOM을 업데이트하는 데 쓰인다. 여기서도 `this.setState`를 사용할 수 있는데, 만약에 조건문으로 감싸주지 않는다면 this.setState가 계속해서 호출되는 일이 발생할 수 있다. (state 업데이트 → 컴포넌트 업데이트 →`componentDidUpdate` 호출 → state 업데이트 → 반복…) ```typescript componentDidUpdate(prevProps: Props, prevState: State) { // 새로운 userName이 props로 전달되었을 때만 데이터를 불러온다. if (this.props.userName !== prevProps.userName) { this.fetchData(this.props.userName); } } ``` #### `componentWillUnmount` 컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출된다. 클린업 함수를 호출하기 위한 최적의 위치이고, 이 메서드 안에서는 `this.setState`를 호출할 수 없다. #### `shouldComponentUpdate` state나 props가 업데이트되면 자연스럽게 컴포넌트도 업데이트된다. 하지만 특정 변화는 컴포넌트에 영향을 주지 않도록 하고 싶다면 이 메서드를 사용하면 된다. state나 props가 변경되었을 때 컴포넌트가 변경되는 것이 굉장히 자연스러운 것이므로 이 메서드는 특별한 성능 최적화 상황에서만 고려하는 것이 좋다. > [!tips]- shouldComponentUpdate를 실제로 어떤 상황에서 쓸 수 있을까? > 실제로는 거의 사용하지 않고, 사용하지 않는 것이 좋다. 최적화를 위해서 `shouldComponentUpdate`를 직접 커스텀하는 것 보다는 `PureComponent`를 사용하는 것이 더 좋다. > React에서는 그 자체로도 리렌더링을 최소화하려는 로직을 가지고 있기 때문에 대부분의 경우는 `shouldComponentUpdate`가 필요 없을 가능성이 높다. > [React Beginner Question Thread ⚛ - DEV Community](https://dev.to/dan_abramov/react-beginner-question-thread--1i5e#comment-1nam) `React.Component`와 `React.PureComponent`는 이 `shouldComponentUpdate`의 구현에 차이가 있다. - `React.PureComponent`의 `shouldComponentUpdate()`는 얕은 비교를 수행한다. - `React.Component`는 `shouldComponentUpdate()`가 구현되어 있지 않아서 상태가 업데이트되는 대로 렌더링이 발생한다. https://legacy.reactjs.org/docs/react-api.html#reactpurecomponent ```tsx type Props = Record<string, never>; interface State { count: number; } const STATIC_STATE = { count: 1 }; class ReactComponent extends React.Component<Props, State> { private renderCounter = 0; constructor(props: Props) { super(props); this.state = { ...STATIC_STATE, }; } private handleClick = () => { this.setState({ ...STATIC_STATE }); }; public render() { console.log('ReactComponent', ++this.renderCounter); return ( <> <h3>state: {this.state.count}</h3> <button onClick={this.handleClick}>update state</button> </> ); } } class ReactPureComponent extends React.PureComponent<Props, State> { private renderCounter = 0; constructor(props: Props) { super(props); this.state = { ...STATIC_STATE, }; } private handleClick = () => { this.setState({ ...STATIC_STATE }); }; public render() { console.log('ReactPureComponent', ++this.renderCounter); return ( <> <h3>state: {this.state.count}</h3> <button onClick={this.handleClick}>update state</button> </> ); } } ``` `React.Component`는 `this.setState`가 호출되는 대로 리렌더링이 발생하지만, `React.PureComponent`는 `shouldComponentUpdate`에서 state 비교를 수행해서 실제로 값이 같은지를 확인해서 달라진 경우에만 리렌더링을 수행한다. 예제 코드에서도 `count`값이 계속 1로 동일하기 때문에 `PureComponent`에서는 리렌더링이 수행되지 않았다. `PureComponent`는 실제 값의 변경을 확인한다는 점에서 최적화 측면에 유용하기 쓰일 수 있지만, 실제로 비교를 수행하기 때문에 성능 이슈가 발생할 수 있다. 얕은 비교 결과 불일치하는 경우가 잦다면 PureComponent를 사용할 필요가 없을 것이다. 그래서 적재 적소에 PureCompoent를 사용해주는 것이 좋다. (관련 자세한 내용은 [[2.5 메모이제이션]]) ![[8de94ea3-8f20-428c-bb7f-e38701370408.png]] #### `static getDerivedStateFromProps` `render()`를 호출하기 직전에 호출되는 메서드이다. 지금은 사라진 `componentWillReceivedProps`를 대체할 수 있는 메서드이고, `static`으로 선언되어 있기 때문에 메서드 안에서 `this`에 접근할 수 없다. 이 메서드의 반환값은 모두 `state`에 들어간다. 만약에 `null`을 반환하면 아무 일도 일어나지 않는다. `props`를 받아서 `state`를 변경하고 싶을 때 사용할 수 있다. 주의해야 할 점은 이 메서드도 모든 `render()` 실행 시에 호출된다는 것이다. > [!tips]- getDerivedStateFromProps가 모든 render()에서 호출되는 것이 왜 주의사항일까? > - 내부 상태를 의도치 않게 리셋할 수 있음 > ```typescript > static getDerivedStateFromProps(props: Props, state: State) { > // props 변경 여부와 관계없이 매번 state를 덮어씀 > return { > // 사용자가 입력한 searchQuery가 계속 초기값으로 리셋됨 > searchQuery: props.initialQuery, > }; > } > ``` > - 매 랜더링마다 불필요한 연산을 실행하게 될 수 있음 > ```typescript > static getDerivedStateFromProps(props: Props, state: State) { > // 무거운 연산이 매 렌더링마다 실행 > // props.data가 안 바뀌어도 계속 실행됨 > const processedData = expensiveProcessing(props.data); > return { processedData }; > } > ``` #### `getSnapshotBeforeUpdate` DOM이 업데이트되기 직전에 호출되는 메서드이고, `componentWillUpdate`를 대체할 수 있는 메서드이다. 여기서 반환되는 값은 `componentDidUpdate`로 전달된다. DOM에 렌더링 되기 전에 윈도우 크기를 조절하거나 스크롤 위치를 조정하는 등의 작업을 처리하는데 유용하게 사용할 수 있다. > [!tips] > `getSnapshotBeforeUpdate`는 반드시 `componentDidUpdate`와 함께 사용해야 한다. (경고 메시지가 뜸) ```typescript // 시나리오: 목록(예를들면 채팅방)에서 새로운 아이템이 추가될 때 스크롤 위치를 유지하기 getSnapshotBeforeUpdate(prevProps: Props, prevState: State) { // props로 넘겨받은 배열의 길이가 이전보다 길어질 경우 (새로운 아이템이 추가됨) // 현재 스크롤 높이 값을 반환한다. if (prevProps.list.length < this.props.list.length) { const list = this.listRef.current; // DOM 업데이트 전 현재 스크롤 위치를 계산 // scrollHeight: 전체 콘텐츠 높이 // scrollTop: 현재 스크롤된 높이 // 둘의 차이 = 스크롤 아래 남은 공간 return list.scrollHeight - list.scrollTop; } return null } // getSnapshotDidUpdate에서 전달한 값은 snapsot에서 받을 수 있다. // snapshot은 클래스 제네릭의 3번째 인수로 넣어줄 수 있다. componentDidUpdate(prevProps: Props, prevState: State, snapshot: Snapshot) { // snapshot 값이 있다면 스크롤 위치를 재조정해서 // 기존 아이템이 스크롤에서 밀리지 않도록 한다. if (snapshot !== null) { const list = this.listRef.current; // 새 메시지가 추가되어 scrollHeight가 증가함 // 증가한 높이에서 이전 "아래 남은 공간"을 빼면 // 기존 메시지가 같은 위치에 보이도록 스크롤 조정 list.scrollTop = list.scrollHeight - snapshot; } } ``` > [!tips]- 이거 함수형 컴포넌트에서는 어떻게 할 수 있는지 > 정확히 대응되는 hook이 없지만, `useLayoutEffect`로 유사하게 구현할 수 있다. > `getSnapshotBeforeUpdate`는 DOM 업데이트 직전에 실행되고, `useLayoutEffect`는 DOM 업데이트 직후에 실행되지만, 둘 다 브라우저 페인팅 전이라서 사용자가 차이를 느끼지 못한다는 공통점이 있다. > ```typescript > useLayoutEffect(() => { > const list = listRef.current; > if (!list) return; > > // 새 아이템이 추가되었을 때 > if (props.list.length > prevMessagesLengthRef.current) { > const snapshot = list.scrollHeight - list.scrollTop; > list.scrollTop = list.scrollHeight - snapshot; > } > > prevMessagesLengthRef.current = messages.length; > }, [messages]); > ``` ![[d5cd421e-00c9-42f1-9dae-2b4a35209214.png]] #### `getDerivedStateFromError` 자식 컴포넌트에서 에러가 발생했을 때 발생하는 메서드이다. 에러가 발생했을 때 정의해 둔 state를 반환하기 위해서 사용하는 메서드이고 이 메서드를 이용해서 에러 처리 로직을 구현할 수 있다. 렌더링 과정에서 호출되는 메서드이기 때문에 `getDerivedStateFromError` 메서드는 부수 효과를 발생시켜서는 안되는데, 여기서 말하는 부수 효과에는 에러 로깅 작업도 포함된다. 에러상황과 관련된 부수 효과 작업은 `getDerivedStateFromError` 메서드 대신 [[componentDidCatch]] 메서드에서 수행하면 된다. > [!note] > `getDerivedStateFromError` 메서드 안에서 부수 효과를 발생시킨다고 해서 에러가 발생하진 않지만, 이 메서드는 렌더링 과정 중에 호출되는 메서드이기 때문에 부수 효과가 렌더링 과정을 불필요하게 방해할 수도 있기 때문에 굳이 여기서 수행할 필요는 없다. ```tsx type Props = React.PropsWithChildren<{}>; type State = { hasError: boolean; errorMessage: string }; class ErrorBoundary extends React.Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false, errorMessage: '', }; } // 자식 컴포넌트에서 발생한 에러를 받아서 state로 만들어줌 static getDerivedStateFromError(error: Error) { return { hasError: true, errorMessage: error.toString(), }; } render() { // 에러가 발생했을때 렌더링할 UI if (this.state.hasError) { return ( <div> <h1>에러가 발생했습니다.</h1> <p>{this.state.errorMessage}</p> </div> ); } // 에러가 발생하지 않았다면 자식 컴포넌트 그대로 렌더링 return this.props.children; } } export default function App() { return ( <ErrorBoundary> <Child /> </ErrorBoundary> ); } ``` #### `componentDidCatch` `componentDidCatch`도 자식 컴포넌트에서 에러가 발생했을 때 호출되는데, `getDerivedStateFromError` 메서드가 실행되어 `state`가 결정된 뒤에 실행된다. 2개의 인수를 받는데, `getDerivedStateFromError`가 받는 것과 동일한 Error와 에러가 발생한 컴포넌트에 대한 info이다. ```tsx componentDidCatch(error: Error, info: React.ErrorInfo) { console.error(error); console.log(info) } ``` `getDerivedStateFromError` 메서드는 render 단계에서 수행되지만, `componentDidCatch` 메서드는 commit 단계에서 수행되기 때문에 `componentDidCatch`에서 부수 효과를 수행하는 것이 더 좋다. (참고: [[2.4 렌더링은 어떻게 일어나는가?#렌더와 커밋|렌더와 커밋]]) > [!note] `componentDidCatch`와 개발/프로덕션 모드 > `componentDidCatch`는 개발 모드와 프로덕션 모드에서 다르게 동작한다. > 개발 모드에서는 `componentDidCatch`에서 처리했는지와 관계 없이 모든 에러가 최상위까지 전파되지만, 프로덕션 모드에서는 `componentDidCatch`에서 잡지 않은 에러만 상위로 전파된다. > [!note] 컴포넌트의 displayName > `componentDidCatch`의 두번째 인수인 errorInfo의 componentStack에는 어떤 컴포넌트에서 에러가 발생했는지를 확인할 수 있는 데이터가 담겨 있는데, 여기에 표시되는 이름은 `Function.name` 또는 컴포넌트의 displayName이다. 만약에 컴포넌트 이름을 추론할 수 없는 경우에는 한눈에 파악하기 어려운 이름이 출력되기 때문에, 추적을 용이하게 하려면 기명 함수 또는 displayName을 쓰는 습관을 들이는게 좋다. > [!note] > `getSnapshotBeforeUpdate`, `getDerivedStateFromError`, `componentDidCatch` 는 아직 함수 컴포넌트에 구현되지 않은 기능이기 때문에 이 기능이 필요한 경우에는 클래스 컴포넌트를 사용해야 한다. ### 클래스 컴포넌트의 한계 클래스 컴포넌트만으로도 완성도 있는 어플리케이션을 만들 수 있겠지만, 함수형 컴포넌트를 이용한 패러다임으로 대세가 옮겨진 이유를 추측해보면 다음과 같다 : - **데이터의 흐름을 추적하기 어렵다** state는 여러 생명주기 메서드 안에서 변경될 수 있다. 그렇기 때문에 어떤 state 변경 흐름으로 렌더링이 일어나는지, 혹은 일어나지 않는지를 파악하기 어렵다. - **애플리케이션 내부 로직의 재사용이 어렵다.** 컴포넌트 간에 중복되는 로직이 있고, 이를 재사용하고 싶다면 고차 컴포넌트(HOC, Higher-Order Component)를 이용하는 방법이 있을 것이다. 그런데 이렇게 고차 컴포넌트를 사용하게 되면 공통 로직이 많아졌을 때 wrapper 컴포넌트나 props가 너무 많아지는 래퍼 지옥(wrapper hell)에 빠질 가능성이 있다. 고차 컴포넌트 외에도 상속을 이용하는 방법도 있지만, 이 역시 상속 흐름을 쫒아야 하기 때문에 복잡도가 증가한다는 문제가 있다. - **기능이 많아질수록 컴포넌트의 크기가 커진다.** 컴포넌트 내부 로직이 많아질수록 생명주기 메서드 사용이 잦아져 컴포넌트 크기가 커진다는 문제가 있다. > [!tips]- 이건 함수형 컴포넌트에서도 동일하게 발생할 수 있는 문제 아닌지? > 맞긴 하다. 하지만 문제의 본질이 다르다. > 함수형 컴포넌트 역시 기능이 많아질수록 컴포넌트의 크기가 커지지만, 함수형 컴포넌트에서는 useEffect를 이용해서 관련 로직의 응집도를 높일 수 있고, 커스텀 훅으로 추출할수도 있다. > 하지만 클래스 컴포넌트는 함수형 컴포넌트와는 달리 생명주기 중심으로 코드가 조직되기 때문에 기능이 많아지면 코드가 분산되어 추적이 어려울 수 있다. - **클래스는 함수에 비해서 상대적으로 어렵다** 자바스크립트는 프로토타입 기반의 언어이고, 자바스크립트에서 클래스의 개념은 비교적 뒤늦게 등장한 개념이기 때문에 자바스크립트 개발자들은 클래스보다는 함수에 더 익숙할 가능성이 높다. 게다가 자바스크립트에서는 다른 클래스 기반 언어들과는 다르게 동작하는 부분도 있기 때문에(this 등…) 기존에 클래스를 다뤄 봤던 개발자에게도 부담이 될 수 있다. - **코드 크기를 최적화하기 어렵다** 클래스 컴포넌트는 최종 결과물인 번들 크기를 줄이는 데에도 어려움이 있다. 클래스 컴포넌트의 번들링 결과를 확인해보면, 이름이 최소화(minified)되지 않고 사용하지 않는 메서드가 트리쉐이킹되지 않는 경우도 있다. ```typescript // 클래스 컴포넌트 번들링 결과 class MyComponent extends React.Component { handleClick() { /* ... */ } handleSubmit() { /* ... */ } // 사용하지 않는 메서드도 번들에 포함될 수 있음 unusedMethod() { /* ... */ } } // 함수형 컴포넌트는 사용하지 않는 함수를 더 쉽게 트리쉐이킹 가능 function MyComponent() { const handleClick = () => { /* ... */ }; const handleSubmit = () => { /* ... */ }; // unusedFunction은 import되지 않으면 번들에서 제외됨 } ``` - **핫 리로딩을 하는 데 상대적으로 불리하다** 클래스 컴포넌트는 최초 렌더링 시에 instance를 생성하고 그 안에서 state 값을 관리하는데, instance 내부 안에 있는 render 메서드를 수정하게 되면 이를 반영하기 위해서는 instance를 새로 생성하는 것 뿐이다. 이 과정에서 state 또한 초기화되어 핫 리로딩시에 state가 초깃값으로 돌아간다. 하지만 함수형 컴포넌트에서는 state를 [[클로저 (Closure)|클로저]]에 저장해두기 때문에 핫 리로딩시에 함수가 다시 실행되더라도 state 값은 유지된다. (자세한 내용은 [[3.1 리액트의 모든 훅 파헤치기]]에서 다룬다.) ## 함수 컴포넌트 ```tsx type Props = { required?: boolean; text: string; }; export function SampleComponent({ required, text }: Props) { const [count, setCount] = useState<number>(0); const [isLimited, setIsLimited] = useState<boolean>(false); function handleClick() { const newValue = count + 1; setCount(newValue); setIsLimited(newValue >= 10); } return ( <> <h2>Sample Component</h2> <div>{required ? '필수' : '선택'}</div> <div>{text}</div> <div>count: {count}</div> <button onClick={handleClick} disabled={isLimited}> 증가 </button> </> ); } ``` 클래스 컴포넌트와 비교했을 때 확실히 간결해졌다. render 안에서 this를 조심할 필요가 없어졌고, state도 객체 형태가 아닌 각각의 원시값으로 다룰 수 있게 되어서 사용하기 편해졌다. ## 함수 컴포넌트 vs 클래스 컴포넌트 ### 생명주기 메서드 생명주기 메서드는 `React.Component`에 구현되어 있고, 클래스 컴포넌트에서는 이를 상속받아서 구현하므로 생명주기 메서드가 존재하지만, 함수형 컴포넌트는 단순히 props를 받아 리액트 요소를 반환하는 함수이기 때문에 생명주기 메서드가 존재하지 않는다. 함수형 컴포넌트에서도 `useEffect`를 이용하여 `componentDidMount`, `componentDidUpdate`, `componentWillUnmount`를 비슷하게 구현할 수 있지만 중요한 것은 "비슷"할 뿐 "똑같다"는 것은 아니라는 것이다. `useEffect`는 생명주기를 대체하기 위한 것이 아니라, 의존성 배열의 값이 변경될 때 부수 효과를 실행하기 위한 훅이다. 관련 내용은 [[2.4 렌더링은 어떻게 일어나는가]]에서 다룬다. ### 함수 컴포넌트와 렌더링된 값 함수 컴포넌트는 렌더링된 값을 고정하고, 클래스 컴포넌트는 그렇지 못한다. [How Are Function Components Different from Classes? — overreacted](https://overreacted.io/how-are-function-components-different-from-classes/) | [번역](https://github.com/gaearon/overreacted.io/blob/archive/src/pages/how-are-function-components-different-from-classes/index.ko.md) [예제 데모 코드샌드박스](https://codesandbox.io/s/pjqnl16lm7) ```tsx class ProfilePage extends React.Component { showMessage = () => { alert('Followed ' + this.props.user); }; handleClick = () => { setTimeout(this.showMessage, 3000); }; render() { return <button onClick={this.handleClick}>Follow</button>; } } ``` follow 버튼을 누르고 프로필을 변경했을 때 - 클래스 컴포넌트는 3초 뒤 변경된 프로필로 alert가 뜨고 - 함수 컴포넌트는 버튼을 누른 시점의 프로필로 alert가 뜬다. 클래스 컴포넌트는 props의 값을 항상 this로부터 가져온다. 문제는 props는 외부에서 변경하지 않는 이상 불변한 값이지만, this는 변경 가능한 값이라는 것이다. 이 경우에서는 this.props의 값이 변경되었기 때문에 3초 뒤 alert 함수에서도 이를 읽을 수 있었던 것이다. 이를 해결할 수 있는 방법 중 하나가 this.props를 일찍 부르고 timeout 함수에 저장해둔 값을 전달하는 것인데. 이 방법은 접근해야 할 props나 state가 많아질수록 코드도 같이 복잡해진다는 문제가 있다. ```tsx class ProfilePage extends React.Component { showMessage = (user) => { alert('Followed ' + user); }; handleClick = () => { const {user} = this.props; setTimeout(() => this.showMessage(user), 3000); }; render() { return <button onClick={this.handleClick}>Follow</button>; } } ``` 함수 컴포넌트에서는 props를 인수로 받는다. 그리고 this와 다르게 인수로 받는 props는 컴포넌트가 이 값을 변경할 수 없고 그대로 사용하게 된다. 이 특성은 state에도 동일하게 적용된다. 함수 컴포넌트는 렌더링이 일어날 때 마다 그 순간의 값인 props와 state를 기준으로 렌더링이 되기 때문에, 값이 변경되더라도 렌더링 시점의 props와 state를 렌더링에 사용한다. 값이 변경되면 다시 한번 그 값을 기준으로 렌더링이 일어난다. ### 클래스 컴포넌트를 공부해야 할까? 클래스 컴포넌트가 사라질 계획은 없어 보인다. (참고: [Hook의 개요 – React](https://ko.legacy.reactjs.org/docs/hooks-intro.html#gradual-adoption-strategy)) 클래스 컴포넌트 대비 함수 컴포넌트가 가지는 장점이 있기 때문에 변경을 고려해볼 만하지만, 이미 잘 동작하는 클래스 컴포넌트를 굳이 함수 컴포넌트로 다시 작성할 필요는 없다.. 리액트 팀에서도 클래스 컴포넌트를 제거할 계획이 없을 뿐더러 클래스 컴포넌트에서 함수 컴포넌트로의 전환은 세심한 주의를 필요로 하기 때문이다. 새로 리액트를 공부하거나 프로젝트를 시작한다면 당연히 함수 컴포넌트로 작성하는 것이 좋지만, 리액트는 오랜 시간 동안 클래스 컴포넌트로 작성되어왔기 때문에 리액트의 흐름을 알기 위해서는 클래스 컴포넌트에 대한 지식을 알고 있으면 좋다. 게다가 ErrorBoundary같이 현재는 클래스 컴포넌트에서만 가능한 기능들도 있기 때문에 이런 점 때문에라도 클래스 컴포넌트에 대한 지식이 있면 좋을 것이다.