[React] 리액트의 생명주기(Lifecycle)

Isabel·2023년 8월 10일
0

CS 및 기술 정리

목록 보기
2/5

컴포넌트는 생명주기를 가짐

  • 생성(mount) → 업데이트(update) → 제거(unmount)
  • 리액트에서 클래스 컴포넌트는 Lifecycle Methods를 사용하고, 함수형 컴포넌트에서는 Hooks를 사용

1. Class Components Lifecycle

1.1. Mount

1.1.1. Constructor

컴포넌트 생성자 메서드
컴포넌트가 생성되면 가장 먼저 실행되는 메서드
this.props, this.state에 접근이 가능하고, React 요소를 반환

1.1.2. getDerivedStateFromProps

props로 받아온 것을 state에 넣어줌

1.1.3. render

화면에 컴포넌트를 렌더링

1.1.4. componentDidMount

컴포넌트의 첫번째 렌더링이 마치면 호출되는 메서드
이 메서드가 호출될 때는 화면에 컴포넌트가 나타난 상태
해당 메서드에서 주로 DOM을 사용해야하는 외부 라이브러리 연동 혹은 해당 컴포넌트에서 필요한 데이터를 요청하기 위한 api 요청 등의 행위를 함

React.useEffect(()=>{
	console.log("componentDidMount");
}, []);

1.2. Update

1.2.1. getDerivedStateFromProps

컴포넌트의 props나 state가 바뀌었을 때도 이 메서드가 호출

1.2.2. shouldComponentUpdate

컴포넌트가 리랜더링할지 말지를 결정하는 메서드
React.memo와 유사하게 boolean return으로 결정

1.2.3. componentDidUpdate

컴포넌트가 업데이트되면 발생

  • 의존성 배열이 변경되었을 때만 useEffect 실행되는 것과 동일
React.useEffect(()=>{
	console.log("countState or exampleProps changed");
}, [countState, exampleProps]);

1.3. Unmount

Unmount는 화면에서 컴포넌트가 사라지는 것

1.3.1. componentWillUnmount

컴포넌트가 화면에서 사라지기 직전에 호출됨
해당 메서드에서 주로 DOM에 직접 등록했던 이벤트를 제거함
외부 라이브러리를 호출한 것이 있고, 해당 라이브러리에 dispose 기능이 있다면 여기서 호출

React.useEffect(()=>{
	const timer = setTimeout(() => {
		console.log('timer');
}, 1000)
	return clearTimeout(timer);
}, []);

2. Functional Components Hooks

React의 함수형 컴포넌트에서 생명주기 관련 기능을 연동할 수 있도록 해주는 함수들
class없이 React를 사용할 수 있게 됨

2.1. React Hooks의 도입 목적

기존 라이프 사이클 메서드 중심이 아니라 로직 중심으로 나눌 수 있게 되어 컴포넌트를 함수 단위로 잘게 쪼갤 수 있게 됨 → 유지보수 EZ

2.2. React Hooks의 사용 규칙

💡 최상단에서만 Hook을 호출할 수 있다.

  • 반복문, 조건문, 중첩된 함수 등에서 호출할 수 없다.
  • 이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 호출되는 것이 보장된다.

💡 리액트 함수 컴포넌트 안에서만 hook을 호출할 수 있다.

  • 일반 JS 안에서는 호출할 수 없다 .

2.3. Hooks의 종류

2.3.1. useState

상태를 관리

const [state, setState] = useState(initialState);

useState의 데이터는 동기적으로 업데이트 되지 않음 (비동기처리)
→ 페이지의 수많은 useState 데이터가 모두 동기적으로 업데이트 된다면 리렌더링을 계속 발생시켜 성능 저하가 일어날 수 있음
→ 이러한 문제를 해결하기 위해서 useState는 setState가 연속적으로 호출되면 데이터를 배치(batch) 처리를 통해 한번에 처리하곤함 (16ms를 기준으로 한 번씩 업데이트된다고 한다. )

2.3.2. useEffect

useEffect(() => {
	
}, [의존성1, 의존성2, ...])

화면에 렌더링 된 이후에 한 번 실행
componentDidMount, componentDidUpdate, 그리고 componentWillUnmount 가 합쳐진 것

  • 만약 화면을 다 그리기 전에 동기화 되어야 하는 경우, useLayoutEffect 를 활용하여 컴포넌트 렌더링 - useLayoutEffect 실행 - 화면 업데이트 순으로 실행시킬 수 있음
  • useLayoutEffect는 렌더링 후 layout과 paint 전에 동기적으로 실행됨
  • useEffect는 렌더링 후, layout과 paint가 완료된 후에 비동기적으로 실행됨
  • useInsertionEffect
  • useEffect 안에서 return은 clean-up을 사용하기 위해서 쓰임
    1. 메모리 누수 방지를 위해 UI에서 컴포넌트를 제거하기 전에 수행
    2. 컴포넌트가 여러 번 렌더링된다면, 다음 effect가 수행되기 전에 이전 effect를 정리

2.3.3. useContext

const ThemeContext = CreateContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={theme}>
      <Form />
      <label>
        <input
          type="checkbox"
          checked={theme === 'dark'}
          onChange={(e) => {
            setTheme(e.target.checked ? 'dark' : 'light')
          }}
        />
        Use dark mode
      </label>
    </ThemeContext.Provider>
  )
}

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <Button>Sign up</Button>
      <Button>Log in</Button>
    </Panel>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
    <section className={className}>
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}

Context API를 통해 만들어진 Context에서 제공하는 value를 가져올 수 있음

컴포넌트에서 가장 가까운 <MyContext.Provider> 이 업데이트되면, 이 hook은 MyContext Provider 에게 전달된 가장 최신의 context value를 사용하여 렌더러를 trigger

전역 상태 관리로 많이 사용됨

2.3.4. useReducer

useState 대체 함수로 컴포넌트 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있움
컴포넌트 바깥에서 로직을 작성할 수 있고, 심지어 다른 파일에서 작성한 후 불러와서 사용할 수도 있음

  • reducer : 현재 상태와 액션 객체를 파라미터로 받아와 새로운 상태를 반환해주는 함수
const [state, dispatch] = useReducer(reducer, initialArg, init?)
import { useReducer } from 'react';

function reducer(state, action) {
  if (action.type === 'incremented_age') {
    return {
      age: state.age + 1
    };
  }
  throw Error('Unknown action.');
}

export default function Counter() {
  const [currentState, dispatch] = useReducer(reducer, { age: 42 }); // 여기에서는 {age : 42}가 initial state
  // ...
 
	return (
    <>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })
      }}>
        Increment age
      </button>
      <p>Hello! You are {state.age}.</p>
    </>
  );

}
  • dispatch function
    • state를 새로운 값으로 변경하게 하고, 리렌더링을 일으킨다.
    • dispatch함수에 매개변수로 action을 넘겨주어야 한다.
    • 기존의 state 값은 dispatch한 action에 따라 변경된 값으로 return된다.
    • action 은 각각을 구분하기 위해 type 프로퍼티를 가진다. (그냥 이름 같은 거)
    • redux를 생각하면 좀 더 이해가 잘 될 듯
function reducer(state, action) {
  switch (action.type) {  //switch ~ case ~ 문으로 조건문 사용
    case 'incremented_age': {
			// state를 직접 변경해서 return 하지 않기! 
			// 새로운 object를 만들어서 return 하기! -> for 불변성
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

function Form() {
  const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
  
  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    });
  }
  // ...

💡 “Too many re-renders”

Too many re-renders. React limits the number of renders to prevent an infinite loop.

렌더링 중에 무조건적으로 액션을 디스패치하여, 컴포넌트가 루프에 진입하게 되는 것

// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>

// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>

// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>

2.3.5. useRef

특정 DOM을 선택할 때 사용되며, 데이터를 저장하는 용도로도 쓰임
.current 프로퍼티로 전달된 인자로 초기화된 변경 가능한 ref 객체를 리턴함
반환된 객체는 컴포넌트의 전 생애 주기를 통해 유지됨

const inputRef = useRef(null);

...

const handleInputClick = () => {
	inputRef.current.focus();
}

<>
	<button onClick={handleInputClick}>click here</button>
	<input ref={ inputRef } />
</>
const valueRef = useRef(0);

...
valueRef.current = valueRef.current + 1;

Performance Hooks

리렌더링을 최적화하는 가장 일반적인 방법은 필요하지 않은 업무를 skip하는 것
리액트가 캐시된 연산 결과를 재사용하게 하거나 이전 렌더링에 비하여 데이터가 바뀌지 않았다면 리렌더링 하지 않게 하는 것

2.3.6. useMemo

const cachedValue = useMemo(calculateValue, dependencies)

고연산의 연산 결과를 캐시할 수 있게 해줌 → 메모이제이션된 값을 반환
의존성이 변경되었을 때만 메모이제이션된 값을 다시 연산함
의존성 배열에 아무런 값도 없는 경우, 매 렌더링마다 새로운 값을 연산함

import { useMemo } from 'react';

function TodoList({ todos, tab, theme }) {
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  // ...
}

최초 렌더시, () => filterTodos(todos, tab) 부분, 즉 사용자가 원하는 연산이 실행되고, 해당 연산의 return 값이 반환됨

매 re-render마다 리액트는 dependencies에 있는 값들을 이전 렌더의 값과 비교할 것

import { useMemo } from 'react';
import { filterTodos } from './utils.js'

export default function TodoList({ todos, theme, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  return (
    <div className={theme}>
      <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ?
              <s>{todo.text}</s> :
              todo.text
            }
          </li>
        ))}
      </ul>
    </div>
  );
}
  • children Component 리렌더링 관리 기본적으로 리액트는 컴포넌트가 리렌더링 되면, 그 자식들도 재귀적으로 모두 리렌더링됨 props가 동일하다면, children 컴포넌트들은 리렌더링을 skip하게 할 수 있음 → React.memo 를 사용!
    import { memo } from 'react';
    
    const List = memo(function List({ items }) {
      // ...
    });
    memo로 감싼다면, 해당 자식 컴포넌트는 props가 모두 바뀌지 않을 경우 리렌더링을 skip함! 이 부분은 useCallback의 경우에도 동일!
    export default function TodoList({ todos, tab, theme }) {
      // Tell React to cache your calculation between re-renders...
      const visibleTodos = useMemo(
        () => filterTodos(todos, tab),
        [todos, tab] // ...so as long as these dependencies don't change...
      );
      return (
        <div className={theme}>
          {/* ...List will receive the same props and can skip re-rendering */}
          <List items={visibleTodos} />
        </div>
      );
    }
    아니면, List 컴포넌트를 useMemo에 넣어도 동일한 결과를 유도할 수 있음
    
    export default function TodoList({ todos, tab, theme }) {
      // Tell React to cache your calculation between re-renders...
      const visibleTodos = useMemo(() => filterTodos(todos, tab),[todos, tab]);
    	const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
      return (
        <div className={theme}>
         { children }
        </div>
      );
    }

2.3.7. useCallback

최적화된 컴포넌트로 넘기기 전에 함수 선언을 캐시할 수 있게 해줌
메모이제이션 된 콜백 함수를 반환
의존성이 변경되었을 때만 변경되며, 특정 함수를 새로 렌더링하지 않고 재사용할 수 있게 함

const cachedFn = useCallback(fn, dependencies)

함수를 캐시하기 위해서는 해당 함수를 useCallback으로 감쌈
handleSubmit이 리턴되는 함수임

import { useCallback } from 'react';

function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // dependencies
  // ...

	return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );

자바스크립트에서는 function(){} , () ⇒ {} 은 항상 다른 함수를 생성함 → 이 말은 렌더링 될 때마다 다른 함수를 생성한다는 의미

useCallback 을 사용하면 같은 함수가 계속 사용됨 (dependencies가 변경되지 않는 한)

useCallback은 아래와 같은 경우에 유용함

  • 함수를 memo로 감싸진 컴포넌트에게 prop으로 보내는 경우 → 이 경우는 개발자가 최적화를 원하는 경우니까
  • 개발자가 넘기는 함수가 나중에 다른 Hook의 dependency로 사용되는 경우

React.memo

props가 변경되지 않았을 때는 컴포넌트가 리렌더링을 skip할 수 있게 해줌

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
export function Movie({ title, releaseDate }) {
  return (
    <div>
      <div>Movie title: {title}</div>
      <div>Release date: {releaseDate}</div>
    </div>
  );
}

export const MemoizedMovie = React.memo(Movie);
  • 같은 props로 리렌더링이 자주 일어나는 컴포넌트에 사용할 것!

1개의 댓글

comment-user-thumbnail
2023년 8월 10일

좋은 정보 얻어갑니다, 감사합니다.

답글 달기