React Hooks

이명제·2022년 11월 18일
0

React Hooks란?

요약하자면 Hook은 함수형 컴포넌트가 클래스형 컴포넌트의 기능을 사용할 수 있도록 해주는 기능입니다.

Hooks 필요성?

hook을 사용해 함수형 컴포넌트에서도 state와 생명주기를 다룰 수 있기에 클래스형 컴포넌트에서만 가능하던 상태관리를 더 손쉽게 할 수 있어 필요합니다.

주로 쓰이는 Hooks에 대해서 아래에 정리해보았습니다.

1-1. useState

함수형 컴포넌트에서 상태를 관리하기 위해서 만들어졌습니다.

const [count, setCount] = useState<number>(0);

// 상태 변경 (re-rendering 발생)
const handlePress = () => {
	setCount(1);
};

return (
	<TouchableOpacity onPress={handlePress}>
		<Text>{count}</Text>
	</TouchableOpacity>
)

1-2. state 불변성

product.price 값의 변화를 감지하지 못하는 경우

const [product, setProduct] = useState({
	name: '과자',
	price: 2000
});

const handlePress = () => {
	product.price = 3000;
};

return (
	<TouchableOpacity onPress={handlePress}>
		<Text>{product.price}</Text>
	</TouchableOpacity>
)

React는 상태가 변경되면 얕은 비교 즉, 참조값 비교를 통해 이전 값과 현재 값이 같은 객체인지 체크합니다.

해당 경우가 리렌더링이 발생하지 않은 이유는 product 객체의 price라는 프로퍼티만 업데이트 했을뿐 price라는 참조값이 변경된건 아니기때문에 발생하지 않았습니다.

1-3. useState에서 초기 계산이 많이 필요하다면?

  • 개선 전
const Something = (props) => {
	// ⚠️ createRows()는 모든 렌더링에서 호출됩니다
  const [rows, setRows] = useState(createRows(props.count));
	
	return (
		...
	)
};

export default Something;
  • 개선 후
const Something = (props) => {
	// ✅createRows()는 한 번만 호출됩니다
  const [rows, setRows] = useState(() => createRows(props.count));
  
	return (
		...
	)
};

export default Something;

1-4. (번외) re-rendering 되는 조건들

  1. state 변경이 있을 때 (setState에 의해서)
  2. props가 업데이트 됐을 때 (전달받은 props가 업데이트 됐다면 렌더링)
  3. 부모 컴포넌트가 업데이트 될 때
  4. forceUpdate()의 경우 (해당 부분은 class만 해당합니다)

2-1. useRef

일반적인 사용처는 자식에게 명령적으로 접근하는 경우입니다.

다양한 사용처가 있으며, 그 사용처는 아래와 같습니다.

  1. DOM 노드나 React Element에 직접 접근
import React, { useRef } from 'react';

...
const inputRef = useRef<TextInput>(null);

...
useEffect(() => {
	// 이런 형태로 Element에 접근이 가능합니다
	inputRef.current.focus();
}, []);

return (
	<>
		<TextInput
			ref={inputRef}
			...
		>
	</>
)
  1. 컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리
import React, { useRef } from 'react';

...
const productInfoRef = useRef<ItemDto>();

...
// 상품 정보를 useRef에 담는다
const handleSaveProductInfo = (item: ItemDto) => {
	productInfoRef.current = item;
}

return (
<>
	...
	<div onClick={() => handleSaveProductInfo(item)}>
		상품 정보를 저장
	</div>
</>
)

2-2. useRef 초기값 지정

위 2가지 케이스에서 초기값 지정은 어떻게 해줘야 할까?

  1. DOM을 직접 조작하기 위한 용도

    // DOM을 직접 조작하기 위해 프로퍼티로 useRef 객체를 사용할 경우, RefObject<T>를 사용해야 하므로 초깃값으로 null을 넣어주자
    const inputRef = useRef<HTMLInputElement>(null);
  2. 로컬 변수 용도

    // 로컬 변수 용도로 useRef를 사용하는 경우, MutableRefObject<T>를 사용해야 하므로 제네릭 타입과 같은 타입의 초깃값을 넣어주자
    const localVarRef = useRef<number>(0);

2-3. ref를 props로 넘겼을 때, 상태값이 변한다면 렌더링하는가?

// A.tsx
...
const aRef = useRef

3-1. useEffect

useEffect 함수는 렌더링 될 때 마다 특정작업을 실행할 수 있도록 하는 hook입니다.

이는 기존 클래스 형식에서 쓰던 생명주기 메소드를 함수형 컴포넌트에서도 사용할 수 있게 된 것입니다.

동작 순서는 useEffect로 전달된 함수는 지연 이벤트 동안에 레이아웃 배치와 그리기를 완료한 
발생합니다.

3-2. useEffect 사용법

  1. 컴포넌트가 마운트될 때 (= 처음 나타났을 때)
// 배열안을 생략하면 마운트될 때만 작동합니다
useEffect(() => {
	console.log('마운트될 때 실행!')
}, [])
  1. dependency(= 배열) 안에 값을 넣을 때
const [visible, setVisible] = useState(false);

// 마운트 할때, visible의 값이 바뀌기 직전에 호출
useEffect(() => {
	console.log('visible값이 변경!');

// 이 부분이 dps
}, [visible]);
  1. cleanup의 경우
useEffect(() => {
	console.log('마운트 때 작동');

	// cleanup 함수 반환 (return 뒤에 나오며 useEffect 뒷정리 함수라고 합니다)
	return () => {
		// 언마운트는 벗어날 때를 뜻합니다
		console.log('언마운트 때 작동');
	}
}, []);

4-1. useLayoutEffect

useLayoutEffect 는 컴포넌트들이 render된 후 실행되며, paint 되기 전에 실행됩니다. 이 작업은 동기적으로 실행됩니다. 또한, dom 을 조작하는 코드가 존재하더라도 사용자는 깜빡임을 경험하지 않습니다.

4-2. 사용법

사용법은 useEffect와 동일합니다.

useLayoutEffect(() => {
	console.log('동작');
}, []);

4-2. useEffect 와 useLayoutEffect의 차이점?

  1. useEffect는 컴포넌트들이 render와 paint 된 후 실행됩니다. 비동기적으로 실행됩니다. paint 된 후 실행되기 때문에, useEffect 내부에 dom 에 영향을 주는 코드가 있을 경우 사용자 입장에서는 화면의 깜빡임을 보게됩니다
  2. useLayoutEffect 는 컴포넌트들이 render된 후 실행되며, paint 되기 전에 실행됩니다. 이 작업은 동기적으로 실행됩니다. 또한, dom 을 조작하는 코드가 존재하더라도 사용자는 깜빡임을 경험하지 않습니다.
  3. 코드로 비교
const Screen = () => {
  const [value, setValue] = useState<number>(0);

	// 이 동작은 paint 되기전 실행함으로 value가 바뀌면서 렌더링될때 깜빡이는 현상을 캐치하지 않습니다
  useLayoutEffect(() => {
    if (value === 0) {
      setValue(10);
    }
  }, [value]);

	// 반면 아래 동작은 value가 바뀌면서 깜빡임을 보이기때문에 사용자가 불편함을 느낄 수 있습니다 
  useEffect(() => {
    if (value === 0) {
      setValue(10);
    }
  }, [value]);

  return (
    <button onClick={() => setValue(0)}>
			{value}
    </button>
  );
};

4-3. 그렇다면 어느 상황에 useEffect & useLayouyEffect를 쓰는게 적당한가?

useLayoutEffect 는 동기적으로 실행되고 내부의 코드가 모두 실행된 후 painting 작업을 거칩니다. 따라서 로직이 복잡할 경우 사용자가 레이아웃을 보는데까지 시간이 오래걸린다는 단점이 있어, 기본적으로는 항상 useEffect 만을 사용하는 것을 권장합니다. 구체적인 예시로는

  • 데이터 fetch
  • event handler
  • state reset

등의 작업은 항상 useEffect 를 사용하되, 화면이 깜빡거리는 상황일 때, 예를들어 위와같이(4-2의 코드 비교 케이스같이) state 이 존재하며, 해당 state 이 조건에 따라 첫 painting 시 다르게 렌더링 되어야 할 때는 useEffect 사용 시 처음에 0이 보여지고 이후에 re-rendering 되며 화면이 깜빡거려지기 때문에 useLayoutEffect 를 사용하는 것이 바람직 합니다.


5-1. useMemo

useMemo는 메모이제이션된 값을 return하는 hook이며, 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화 하는 용도로 사용됩니다.

(*메모이제이션이란? 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술입니다)

5-2. 일반적인 useMemo 사용법

import { useMemo, useEffect, useState } from 'react'; 

function App() {
  const [number, setNumber] = useState(0);
  const [isKorea, setIsKorea] = useState(true);
  
  // const location = { country: isKorea ? '한국' : '일본' };
  const location = useMemo(() => {
    return {
      country: isKorea ? '한국' : '일본'
    }
  }, [isKorea])

  return (
    <header className="App-header">
        <h2>하루에 몇 끼 먹어요?</h2>
        <input type="number" value={number} onChange={(e) => setNumber(e.target.value)}/>
        <hr/>

        <h2>어느 나라에 있어요?</h2>
        <p>나라: {location.country}</p>
        <button onClick={() => setIsKorea(!isKorea)}>Update</button>
    </header>
  );
}

export default App;

5-3. useMemo 활용하기

  1. useMemo를 사용하지 않았을때의 경우
import React, { useState } from 'react';

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');

  const getAverage = () => {
    console.log('평균값 계산 중...');
    if (list.length === 0) return 0;
    const sum = list.reduce((a, b) => a + b);
    return sum / list.length;
  };

  const onChange = (e) => {
    setNumber(e.target.value);
  };

  const onInsert = () => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
  };

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {getAverage()}
      </div>
    </div>
  );
};

export default Average;

결과는?

  • 글자 입력 시에도 getAverage함수가 동작하기 때문에 콘솔창에 "평균값 계산중... "이 뜸
  • 버튼 클릭시에도 getAverage함수가 동작하기 때문에 콘솔창에 "평균값 계산중..."이 뜸
  • 종합적으로, getAverage함수가 필요하지 않은 동작에서도 작동이 됩니다.

이유는?

위에서 언급했듯이 re-rendering 되는 조건들에 해당합니다.

  1. state 변경이 있을 때 (setState에 의해서)

  2. props가 업데이트 됐을 때 (전달받은 props가 업데이트 됐다면 렌더링)

  3. 부모 컴포넌트가 업데이트 될 때

  4. useMemo를 사용했을 때의 경우

import React, { useState, useMemo } from 'react';

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');
  
  const getAverage = useMemo(() => {
    console.log('평균값 계산 중...');
    if (list.length === 0) return 0;
    const sum = list.reduce((a, b) => a + b);
    return sum / list.length;
  }, [list]); //list값이 업데이트 될때만 실행

  const onChange = (e) => {
    setNumber(e.target.value);
  };

  const onInsert = () => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
  };

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {getAverage} 
        {/* useMemo는 값을 반환 */} 
      </div>
    </div>
  );
};

export default Average;

결과는?

  • 첫 렌더링과 값 입력 할때만 렌더링, 즉 [ ] 안에(=dps) 써준 값 list가 업데이트 될 때만 렌더링.

5-4. 주의사항

  1. 재계산하는 함수가 아주 간단하다면 성능상의 차이는 아주 미미하겠지만 만약 재계산하는 로직이 복잡하다면 불필요하게 비싼 계산을 하는 것을 막을 수 있습니다.
  2. 간단한 로직에 useMemo를 남발하면 오히려 성능 저하를 일으킵니다.

6-1. useCallBack

useMemo는 메모이제이션된 함수를 return하는 hook이며, 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화 하는 용도로 사용됩니다.

(*메모이제이션이란? 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술입니다)

6-2. useCallBack 활용법

  1. useCallback 을 사용하지 않는다면?
...

const onRemove = (id) => {
  setUsers(users.filter(user => user.id !== id));
};

const onToggle = (id) => {
  setUsers(
    users.map(user =>
      user.id === id ? { ...user, active: !user.active } : user
    )
  );
};

return (
	...
)

위 함수들은 컴포넌트가 리렌더링 될 때 마다 새로 만들어집니다.

함수를 선언하는 것 자체는 사실 메모리도, CPU 도 리소스를 많이 차지 하는 작업은 아니기 때문에 함수를 새로 선언한다고 해서 그 자체 만으로 큰 부하가 생길일은 없지만, 한번 만든 함수를 필요할때만 새로 만들고 재사용하는 것은 여전히 중요합니다.

그 이유는, 우리가 나중에 컴포넌트에서 props 가 바뀌지 않았으면 Virtual DOM 에 새로 렌더링하는 것 조차 하지 않고 컴포넌트의 결과물을 재사용 하는 최적화 작업을 할건데요, 이 작업을 하려면, 함수를 재사용하는것이 필수입니다.

  1. useCallBack을 사용한다면?
...

const onRemove = useCallback((id) => {
	setUsers(users.filter(user => user.id !== id));
}, [users]);

const onToggle = useCallback((id) => {
	setUsers(users.map(user =>
		user.id === id ? { ...user, active: !user.active } : user
	));
}, [users]);

...
return (
	...
)

6-3. useMemo 와 useCallback 의 차이?

  1. useMemo는 ‘값’ 을 반환, useCallback은 ‘함수’ 를 반환한다는 차이가 있습니다.
  2. useMemo는 저장했다가 dps가 변한다면 기존 값을 반환하지만, useCallback은 dps가 변하면서 함수를 반환할 때, 형태가 같더라도 새로운 함수를 반환합니다.

  1. useCallback은 useMemo 를 기반으로 만들어졌습니다. 다만, 함수를 위해서 사용 할 때 더욱 편하게 해준 것 뿐입니다. 이 사실을 이용해서 아래와 같은 식으로도 표현 할 수 있습니다.

    const onToggle = useMemo(() => {
    	/* ... */
    }, [users]);

6-4. useCallback 주의사항

  1. 재계산하는 함수가 아주 간단하다면 성능상의 차이는 아주 미미하겠지만 만약 재계산하는 로직이 복잡하다면 불필요하게 비싼 계산을 하는 것을 막을 수 있습니다.
  2. 간단한 로직에 useCallback를 남발하면 오히려 성능 저하를 일으킵니다.

6-5. useCallback & useMemo 공통된 주의사항

  • mobx와 혼용하는 경우, 반드시 dps에 mobx state를 넣어주어야 합니다
import React, { useState, useCallback } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { useStore } from 'stores/StoreHelper';
import { observer } from 'mobx-react';

const T = observer(() => {
  const [count, setCount] = useState<number>(0);
  const { itemStore } = useStore();

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

  /** 어딘가에서 가져온 컴포넌트라고 가정한다 */
  const SomeComponent = useCallback(() => {
    return (
      <View>
        <Text>somethings</Text>
        {/* mobx에서 가져온 state 입니다 */}
        <Text>{itemStore.item?.name}</Text>
      </View>
    );
  }, [itemStore.item?.name]);

  return (
    <View>
      <TouchableOpacity onPress={handleChangeCount}>
        <Text>버튼</Text>
      </TouchableOpacity>
      <Text>{`현재 숫자는 ${count}`}</Text>
      <SomeComponent />
    </View>
  );
});

export default T;

7-1. memo

React.memo는 Higher-Order Components(HOC)입니다.

(HOC란 컴포넌트를 인자로 받아서 새로운 컴포넌트를 return해주는 구조의 함수)

useMemo, useCallback과 마찬가지로 불필요한 렌더링을 방지하기 위해서 사용됩니다.

7-2. memo 사용법

일반적으로 아래와 같은 형태로 사용됩니다.

const Welcome = ({ name }) => {
  return <h1>Hello { name }</h1>;
};

export default React.memo(Welcome);

-----------------------혹은--------------

const MyComponent = (props) => {
  /* 컴포넌트 렌더링 코드 */
};

const somethings(prevProps, nextProps) {
  /*
  만약 전달되는 nextProps가 prevProps와 같다면 true를 반환, 같지 않다면 false를 반환
  */
};

export default React.memo(MyComponent, somethings);

7-3. 그렇다면 useMemo와 memo가 다른점은?

  1. memo는 HOC, useMemo는 hook
  2. memo는 HOC이기 때문에 클래스형 컴포넌트, 함수형 컴포넌트 모두 사용 가능하지만, useMemo는 hook이기 때문에 오직 함수형 컴포넌트 안에서만 사용 가능하다.

7-4. 정리

useCallback & useMemo & memo 는 사용법을 제대로 알고 정확히 쓰지 않는다면 불필요한 연산만 추가되는 꼴이 되버릴 수 있으니 주의 혹은 아예 사용하지 않는 것을 고려해야할 것 같습니다.

0개의 댓글

관련 채용 정보