함수 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅이다.
아무런 값을 넘겨주지 않으면 초깃값은 undefined이다.
function Component() {
const [, triggerRender] = useState();
let state = 'hello';
function handleButtonClick(){
state = 'hi'
triggerRender();
}
return(
<>
<h1>{state}</h1>
<button onClick={handleButtonClick}>hi</button>
</>
)
}
다음과 같은 예시는 버튼을 클릭해도 변경된 state값이 렌더링 되지 않는다.
리액트의 렌더링은 함수 컴포넌트에서 반환한 결과물인 return의 값을 비교해 실행되기 때문이다. 매번 렌더링이 발생될 때마다 함수는 다시 새롭게 실행되고, 새롭게 실행되는 함수에서 state는 매번 hello로 초기화되므로 아무리 state를 변경해도 다시 hello로 초기화되는 것이다.
이를 해결하기 위해 리액트는 클로저를 이용했다. useState 내부에 선언된 setState가 함수의 실행이 종료된 이후에도 지역변수인 state를 계속 참조할 수 있다는 것을 의미한다.
게으른 초기화
useState 인수로 특정한 값을 넘기는 함수를 인수로 넣어줄 수도 있다. 이를 게으른 초기화라고 한다.
게으른 초기화는 useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하라고 돼 있다. 오로지 state가 처음 만들어질 때만 사용된다. 이후 리렌더링이 발생된다면 이 함수의 실행은 무시된다.
따라서 localStorage나 sessionSrotage에 대한 접근, map, filter, find 같은 배열에 대한 접근, 혹은 초깃값 계산을 위해 함수 호출이 필요할 때와 같이 무거운 연산을 포함해 실행 비용이 많이 드는 경우에 사용하는 것이 좋다.
useEffect란?
첫번째 인수로는 실행할 부수효과가 포함된 함수를, 두번째 인수로는 의존성 배열을 전달한다. 렌더링을 할때마다 의존성에 있는 값을 보면서 이 의존성의 값이 이전과 다른 게 하나라도 있으면 부수효과를 실행하는 평범한 함수라 볼 수 있다.
따라서 useEffect는 state와 props의 변화 속에서 일어나는 렌더링과정에서 실행되는 부수 효과 함수이다.
클린업 함수의 목적
클린업 함수는 새로운 값을 기반으로 렌더링 뒤에 실행되지만 변경된 값을 읽는 것이 아니라 함수가 정의됐을 당시에 선언됐던 이전 값을 보고 실행된다.
콜백이 실행될 때마다 이전의 클린업 함수가 존재한다면 그 클린업 함수를 실행한 뒤에 콜백을 실행한다. 따라서 이전에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가해 특정 이벤트의 핸들러가 무한히 추가되는 것을 방지할 수 있다.
의존성 배열
의존성 배열은 빈 배열을 두거나, 아무런 값도 넘기지 않거나, 사용자가 직접 원하는 값을 넣어줄 수 있다.
useEffect의 구현
의존성 배열의 이전값과 현재 값의 얕은 비교를 통해 하나라도 변경 사항이 있다면 callback으로 선언한 부수효과를 실행한다. 이것이 useEffect의 본질이다.
useEffect를 사용할 때 주의할 점
❓왜 useEffect의 콜백인수로 비동기 함수를 바로 넣을 수 없을까?
useEffect의 인수로 비동기 함수가 사용 가능하다면 비동기 함수의 응답속도에 따라 결과가 이상하게 나타날 수 있다. 이를 경쟁상태라고 하는데, 극단적인 예로 이전 state 기반의 응답이 10초가 걸렸고 이후 바뀐 state기반의 응답이 1초뒤에 왔다면 이전 state 기반으로 결과가 나와버리는 불상사가 생길 수 있다.
useEffect 내부에서 비동기 함수를 선언해 실행하는 것은 가능하다.
비용이 큰 연산에 대한 결과를 저장해두고, 이 저장된 값을 반환하는 훅이다.
첫번째 인수로는 어떠한 값을 반환하는 생성 함수를, 두번째 인수로는 해당 함수가 의존하는 값의 배열을 전달한다. 의존성 배열의 값이 변경되지 않았으면 함수를 재실행하지 않고 이전에 기억해 둔 해당 값을 반환한다.
값이 변경됐다면 첫번째 인수의 함수를 실행한 후에 그 값을 반환하고 다시 기억해둔다.
useMemo가 값을 기억했다면, useCallback은 인수로 넘겨받은 콜백 자체를 기억한다. 특정함수를 새로 만들지 않고 다시 재사용한다는 의미다.
첫번째 인수로 함수를 받고, 두번째 인수로 의존성 배열을 넣으면 useMemo와 마찬가지로 의존성 배열이 변경되지 않는 한 함수를 재생성하지 않는다.
useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있다. 그러나 다음과 같은 차이점이 존재한다.
가장 일반적인 예로는 DOM에 접근하고 싶을 때 사용된다. 유용한 경우는 렌더링을 발생시키지 않고 원하는 상태값을 저장할수 있다는 특징을 활용해 useState의 이전 값을 저장하는 훅을 구현할 때다. 이렇게 개발자가 원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관해두고 싶다면 useRef를 사용하는 것이 좋다.
Context란?
prop drilling은 해당 데이터를 제공하는 쪽이나 사용하는 쪽 모두에게 불편하다. 해당 값을 사용하지 않는 컴포넌트에서도 단순히 값을 전달하기 위해 props가 열려 있어야 하고, 사용하는 쪽도 prop 내려주기가 적용돼 있는지 확인해야 하는 등 매우 번거로운 작업이다.
이런 prop drilling을 극복하기 위해 등장한 개념이 Context이다. props 전달 없이도 선언한 하위 컴포넌트 모두에게 자유롭게 원하는 값을 사용할 수 있다.
Context를 함수 컴포넌트에서 사용할 수 있게 해주는 useContext 훅
const Context = createContext<{hello : string} | undefined>(undefined)
function ParentComponent() {
return(
<>
<Context.Provider value={{ hello : 'react'}}>
<Context.Provider value={{ hello : 'js'}}>
<ChildComponent />
</Context.Provider>
</Context.Provider>
</>
)
}
function ChildComponent() {
const value = useContext(Context);
// react가 아닌 js를 반환한다.
return <>{value ? value.hello : ''}</>
}
useContext를 사용하면 상위 컴포넌트 어딘가에서 선언된 <Context.Provider />에서 제공한 값을 사용할 수 있게 된다. 만약 여러개의 Provider가 있다면 가장 가까운 Provider의 값을 가져오게 된다.
Context가 존재하지 않아 에러가 발생할 수도 있는데, 이러한 에러를 방지하려면 useContext 내부에서 해당 콘텍스트가 존재하는 환경인지 (값을 내려주고 있는지) 확인해보면 된다.
useContext를 사용할 때 주의할 점
함수 컴포넌트 내부에서 사용할 때는 항상 컴포넌트 재활용이 어려워진다는 점을 염두에 둬야 한다. Provider에 의존성을 가지고 있기 때문이다. 따라서 컨텍스트가 미치는 범위는 필요한 환경에서 최대한 좁게 만들어야 한다.
콘텍스트는 상태를 주입해주는 API다. 상태 관리 라이브러리가 되기 위해서는 두가지 조건을 만족해야 한다.
그러나 콘텍스트는 둘 중 어느 것도 하지 못한다. 단순히 props 값을 하위로 전달해줄 뿐 렌더링이 최적화되지는 않는다. 결론적으로는 컴포넌트 트리 전체가 리렌더링 된다.
useState의 심화버전으로 볼 수 있다. 비슷한 형태를 띠지만 좀 더 복잡한 상태값을 미리 정의해 놓은 시나리오에 따라 관리할 수 있다.
복잡한 형태의 state를 사전에 정의된 dispatcher로만 수정할 수 있게 만들어줌으로써 state 값에 대한 접근은 컴포넌트에서만 가능하게 하고, 이를 업데이트 하는 방법에 대한 상세 정의는 컴포넌트 밖에다 둔 다음, state의 업데이트를 미리 정의해 둔 dispatcher로만 제한하는 것이다.
다음과 같은 상황에서 사용하는 것이 좋다.
널리 사용되지는 않는다. 그러나 일부 사례에서 유용하게 활용될 수 있다. useImperativeHandle을 이해하기 위해서는 먼저 forwardRef에 대해 알아야한다.
forwardRef 살펴보기
ref는 useRef에서 반환한 객체로, 리액트 컴포넌트의 props인 ref에 넣어 HTMLElement에 접근하는 용도로 사용된다. 상위 컴포넌트에서는 접근하고 싶은 ref가 있지만 이를 직접 props로 넣어 사용할 수 없을 때 사용된다.
forwardRef가 탄생한 배경은 ref를 전달하는데 있어 일관성을 제공하기 위해서다.
useImperativeHandle이란?
부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅이다.
원래는 ref는 {current : <HTMLElemet>}와 같은 형태로 HTMLElement만 주입할 수 있는 객체였다. 그러나 useImperativeHandle을 사용함으로써 자식 컴포넌트에서 새롭게 설정한 객체의 키와 값에 대해서도 접근할 수 있게 됐다.
공식문서에서는 다음과 같이 정의하고 있다.
“이 함수의 시그니처는 useEffect와 동일하나, 모든 DOM의 변경 후(렌더링)에 동기적으로 발생한다.”
실행순서는 다음과 같다.
여기서 동기적으로 발생한다는 것은 useLayoutEffect의 실행이 종료될 때까지 기다린 다음 화면을 그린다는 것을 의미한다. 이는 컴포넌트가 일시 중지되는 것과 같은 일이 발생한다 .따라서 DOM이 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때만 사용하는 것이 좋다.
일반적으로 프로덕션 웹서비스에서 사용하는 훅이 아니다.
useDebugValue는 사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅이다. 두번째 인수로 포매팅 함수를 전달하면 이에 대한 값이 변경됐을 때만 호출되어 포매팅된 값을 노출한다.
오로지 다른 훅 내부에서만 실행가능하다.
훅의 규칙
훅에 대한 정보 저장을 index와 같은 키를 기반으로 구현돼 있다. 훅은 파이버 객체의 링크드 리스트의 호출 순서에 따라 저장된다. 이렇게 고정된 순서에 의존해 훅과 관련된 정보를 저장함으로써 이전 값에 대한 비교와 실행이 가능해진다.