준영
훅
- 함수 컴포넌트가 상태를 사용하거나 클래스 컴포넌트의 생명주기 메서드를 대체하는 등 다양한 작업을 하기 위한 장치
- 클래스 컴포넌트에서만 가능했던 state, ref 등 리액트 핵심 기능을 함수에서도 사용할 수 있도록 해준다.
- 무엇보다 간결하게 작성할 수 있다.
함수 내부에 일반 변수를 사용할 경우, 왜 렌더링이 일어나지 않을까?
- 리액트에서는 함수 컴포넌트의 return 혹은 클래스 컴포넌트의 render 함수를 실행하여 현재 DOM과 가상 DOM을 비교하여 변경된 부분에 대해서만 리렌더링을 한다.
- 하지만 일반 변수를 사용할 경우, 리액트에서는 업데이트를 인지하지 못하여 렌더링이 일어나지 않는다.
- 또한 함수 컴포넌트에서 일반 변수와 함께 상태 업데이트 함수를 사용하여 리렌더링을 일으키더라도 변경된 값이 렌더링되지 않는다.
- 그 이유는 렌더링시 함수는 새롭게 다시 실행되기 때문에 매번 같은 값으로 초기화되어 렌더링이 일어나지 않는 것이다.
useState
- 함수 컴포넌트에서 상태를 관리할 수 있게 해주는 훅
- 매번 실행되는 함수 컴포넌트 환경에서 state의 값을 유지하고 사용하기 위해 클로저로 구현되어 있다.
- 따라서 외부에 상태 값을 노출시키지 않고 리액트 내부에서만 사용할 수 있고, 매번 다시 실행되더라도 정확하게 이전의 값을 꺼내 사용할 수 있다.
게으른 초기화 (Lazy Initialization)
useState
의 인수로 특정한 값을 반환하는 함수를 넣어 사용할 수 있다.- 오로지 state가 처음 만들어질 때에만 사용되고, 이후에 리렌더링이 될 때에는 실행하지 않고 기존에 실행하여 저장된 값을 가져온다.
언제 사용하는 것이 좋을까?
useState
의 초깃값이 복잡하거나 무거운 연산을 포함하는 경우에 사용하는 것을 권장한다고 한다.- localStorage나 sessionStorage에 대한 접근
- map, filter, find 같은 배열에 대한 접근
- …
useEffect
- 컴포넌트의 여러 값들을 활용하여 부수 효과를 만드는 훅
- 첫 번째 파라미터: 부수 효과가 포함된 함수
- 두 번째 파라미터: 의존성 배열
어떻게 의존성 배열이 변경된 것을 알고 실행될까?
-
함수 컴포넌트는 state나 props에 변경이 생길 때마다 매번 함수를 실행시켜 렌더링을 수행한다.
-
의존성 배열의 이전 값과 현재 값의 얕은 비교, 즉,
Object.is
를 통해 수행된다.✅ 파이버를 통해 변경사항을 수집하게 되고, 이때 의존성 배열에 포함된 값이 수정되었다면 부수 효과를 일으킬 수 있도록 실행되는 것이 아닐까?
✅ 알아보니 아닌 것 같다. 두 사이에는 관계가 없는듯..? 좀더 알아봐야겠다.
클린업 함수
- 함수 컴포넌트가 리렌더링되었을 때 의존성 변화가 있었다면, 함수가 정의되었을 때의 값을 기준으로 실행되는 함수
- useEffect의 return 값으로 전달된다.
- 일반적으로 등록한 이벤트를 지울 때 사용
의존성 배열
- 다음과 같은 배열이 들어갈 수 있다.
- 빈 배열
- 비교할 의존성이 없다.
- 즉, 최초 렌더링 이후 더 이상 실행되지 않는다.
- 아무런 값도 넣지 않음
- 렌더링될 때마다 실행이 필요하다.
- 즉, 매 렌더링마다 실행된다.
- 사용자가 원하는 값을 넣은 배열
- 의존성으로 넣어준 값들이 변경되었을 때에만 실행된다.
- 빈 배열
의존성 배열에 아무런 값도 넣지 않으면 컴포넌트 내부에서 함수를 실행시키는 것과 같지 않나?
- useEffect는 클라이언트 사이드에서 실행이 보장된다.
- useEffect는 컴포넌트 렌더링이 완료된 이후에 실행되지만, 내부에서 실행시키는 함수는 렌더링되는 도중에 실행된다.
- 따라서 렌더링을 지연시킬 수 있다.
useEffect 사용시 주의할 점
eslint-disble-line-react-hooks/exhaustive-deps
주석 피하기- 의존성 배열에 포함돼 있지 않은 값이 있을때 의도치 못한 버그를 발생시킬 수 있다.
- useEffect의 첫 번째 인수에 함수명을 부여하라
- useEffect의 첫 번째 인수로 넘긴 함수의 크기가 커질 경우, 의도를 파악하기 어려울 수 있으니 함수명을 부여한다면 목적을 파악하기 수월해질 수 있다.
- 거대한 useEffect를 만들지 마라
- 부수 효과가 커질수록 성능에 악영향을 끼친다.
- 자바스크립트 실행 성능에 영향을 미치기 떄문
- 최대한 작게 분리하여 만들자.
- 부수 효과가 커질수록 성능에 악영향을 끼친다.
- 불필요한 외부 함수를 만들지 마라
- 관련 함수는 내부로 가져와 작성하는 것이 훨씬 간결할 수 있다.
useEffect의 Callback에 비동기 함수를 넣었을때 발생할 수 있는 문제점
- 비동기 함수의 응답 속도에 따라 결과가 이상하게 나타날 수 있다.
- 이전 state 기반 응답이 10초, 이후 state 기반 응답이 1초 뒤에 왔다면, 이전 state 기반 응답으로 인한 결과가 나와버려 원하는 결과를 얻지 못할 수 있다.
- 즉,
Race condition
이 발생할 수 있다.
- 즉,
- 이전 state 기반 응답이 10초, 이후 state 기반 응답이 1초 뒤에 왔다면, 이전 state 기반 응답으로 인한 결과가 나와버려 원하는 결과를 얻지 못할 수 있다.
- 따라서, useEffect 내부에서 비동기 함수를 실행하거나 즉시 실행 함수를 통해 사용하자!
useMemo
- 비용이 큰 연산에 대한 결과를 저장해두고, 해당 값을 반환하는 훅
- 첫 번째 파라미터: 값을 반환하는 함수
- 두 번째 파라미터: 의존성 배열
- useEffect와 마찬가지로 의존성 배열이 변하지 않는 한 이미 저장되어있는 값을 반환한다.
- 값 뿐만 아니라 컴포넌트를 저장할 수도 있다.
- 물론
React.memo
를 사용하는 쪽이 더 좋다.
- 물론
useCallback
- useMemo와 비슷하지만, 인수로 넘겨받은 콜백 자체를 기억한다.
- 즉, 특정 함수를 재사용한다.
- useMemo를 통해 구현이 가능하다. (실제로 그렇게 구현되어 있던 것으로 기억)
useRef
- 컴포넌트가 렌더링될 때 생성되어 특정 값을 저장한다.
- 인스턴스가 여러 개라도 각각 별개의 값을 바라본다(?)
- 반환 값인 객체 내부의 current로 값에 접근 혹은 변경이 가능하다.
- useRef는 값이 변하더라도 렌더링을 발생시키지 않는다.
- 보통 DOM에 접근하기 위해 사용한다.
Context
- 특정 데이터를 자식에서도 사용하기 위해선 props로 데이터를 전달하는 것이 일반적이다.
- 하지만 이러한 컴포넌트간 거리가 멀어질 경우, 코드는 복잡해진다.
- 조상 ~ 자식까지 계속해서 props를 통해 데이터를 전달해야 하는데, 이를
props drilling
이라고 한다. - 따라서 중간에 해당 데이터를 사용하지 않는 컴포넌트도 drilling을 위해 props로 데이터를 갖고 있어야 하므로 매우 비효율 적이다.
- 조상 ~ 자식까지 계속해서 props를 통해 데이터를 전달해야 하는데, 이를
- 이를 해결하기 위해 등장한 개념이
Context
이다.
useContext
- Context와 해당 Context를 함수 컴포넌트에서 사용할 수 있게 해주는 훅
- 가까운 Provider에 존재하는 값을 가져온다.
- 만약 정의된 Provider가 없다면 undefined가 반환되며 이를 미리 걸러낼 수 있도록 별도의 함수로 감싸 사용하는 것이 좋다.
useContext 사용시 주의할 점
- useContext를 사용한 순간부터 Provider와의 의존성이 생긴다.
- 즉, 재활용하기 어려워질 수 있다.
- 따라서, Context가 미치는 범위를 필요한 환경 내에서 최대한 좁게 만들어야 한다.
- Context가 상태 관리 라이브러리는 아니다.
- 따라서 별다른 렌더링 최적화를 해주지 않기 때문에, 이를 위한 별도의 처리가 필요하다.
- 부모 컴포넌트가 렌더링되었을 때 자식 컴포넌트의 리렌더링 또한 일어나기 때문에,
React.memo
를 사용하는 등의 최적화 장치가 필요하다.
useReducer
- useState와 마찬가지로 상태를 정의하는 훅
action
을 통해 상태 변화를 일으키는 타입들을 정의reducer
를 통해 어떤action
이 상태를 어떻게 업데이트할 지를 정의- 따라서,
dispatcher
를 통해action
을reducer
에 전달하면 상태가 업데이트
- 상태 업데이트를
dispatcher
로 제한할 수 있기 때문에 상태 변경 시나리오가 특정되는 경우 사용하면 좋다.
forwardRef
ref
를 전달하는데 있어 일관성을 제공하기 위해 탄생ref
를 전달 받는 요소임을 확실하게 예측할 수 있다.
useImperativeHandle
-
넘겨 받은
ref
에 동작을 추가할 수 있는 훅-
원래 ref는
{ current: <HTMLElement> }
와 같은 형태지만, 해당 훅을 통해 추가적인 동작을 정의할 수 있다.useImperativeHandle( ref, () => ({ alert: () => alert(props.value), }), [props.value], ); /* ... */ inputRef.current.alert(); // 추가한 동작을 사용할 수 있다.
-
useLayoutEffect
- 함수 시그니처는 useEffect와 동일하나, 모든 DOM의 변경 후에 동기적으로 발생하는 훅
- 여기서 말하는 DOM 변경이란, 브라우저에 실제 반영되는 렌더링이 아닌 리액트의 렌더링이다.
- 실행 순서는 아래와 같다.
- 리액트가 DOM을 업데이트
- useLayoutEffect 실행
- 브라우저에 변경 사항을 반영
- useEffect 실행
- 따라서, DOM은 계산되었지만, 화면에 반영하기 전 수행하고 싶은 작업이 있을 때 사용하면 자연스러운 사용자 경험을 제공할 수 있다.
- DOM 요소를 기반으로 한 애니메이션
- 스크롤 위치 제어
- …
useDebugValue
- 디버깅하고 싶은 정보를 해당 훅에 사용하여 리액트 개발자 도구에서 확인할 수 있다.
- 사용자 정의 훅 내부의 내용에 대한 정보를 담아 확인할 수 있다.
- 오직 다른 훅의 내부에 사용 가능하다.
훅의 규칙
- 아래 규칙을 만족해야만 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다.
- 최상위에서만 훅을 호출해야 한다.
- 반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다.
- 훅들은 리액트 어딘가에 있는
index
와 같은 키를 기반으로 구현되어 순서에 아주 큰 영향을 받는다.- 파이버에서는
next
라는 속성을 통해 다음에 처리될 훅을 순서에 따라 링크드 리스트 형태로 저장한다.- 순서에 의존하여
state
나effect
의 결과에 대한 값을 저장하고, 이를 통해 이전 값과 비교와 실행이 가능하다.
- 순서에 의존하여
- 파이버에서는
- 따라서 순서가 보장되지 않는다면 에러를 일으킨다.
- 조건문이 필요할 경우, 훅 내부에서 수행하자!
사용자 정의 훅
- 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용한다.
- 리액트 훅을 사용하기 때문에 리액트에서만 사용이 가능하며 규칙을 따라야 한다.
고차 컴포넌트
- 컴포넌트 자체의 로직을 재사용하기 위한 방법이다.
- 고차 함수의 일종으로, 자바스크립트의 일급 객체, 함수의 특징을 이용하여 리액트가 아니더라도 쓰일 수 있다.
React.memo
props
변화가 없음에도 발생하는 컴포넌트의 렌더링을 방지하기 위해 만들어진 리액트의 고차 컴포넌트props
가 같다면 메모이제이션된 컴포넌트를 반환한다.useMemo
를 사용할 수도 있지만, 할당식을 사용해야 하기도 하고, 보다 목적과 용도가 뚜렷한memo
를 사용하자.
사용자 정의 훅이 필요한 경우
- 공통 로직을 따로 분리할 때 사용하는 것이 좋다.
- 렌더링에는 영향을 미치지 못하기 때문에 사용이 제한적이며, 따라서 컴포넌트 내부에 미치는 영향을 최소화하여 개발자가 원하는 방향으로 사용할 수 있다.
✅ 렌더링 자체는 컴포넌트 함수 실행으로 이루어진다. 따라서 단순히 복잡한 로직을 선언형으로 만들어 값을 제공하는 용도로만 사용되어 컴포넌트 안에서 개발자가 자유롭게 다룰 수 있어 이렇게 설명한 것 같다.
고차 컴포넌트를 사용해야 하는 경우
- 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 것이 좋다.
✅ 하나의 컴포넌트 내에서 특정 상황에 따라 보여주는 결과물이 다르다면, 고차 컴포넌트를 활용하여 렌더링을 하는 것이 좋다라고 설명하는 것 같다. 사용자 정의 훅만으로는 이를 표현하기에는 어렵기 때문이다.
도은
3.1 리액트의 모든 훅 파헤치기
- 함수 컴포넌트에서 가장 중요한 개념은 훅
3.1.1 useState
- 함수 컴포넌트 내부에서 상태를 정의
- 이 상태를 관리할 수 있게 해주는 훅
useState 구현 살펴보기
- useState의 인수로는 사용할 state의 초깃값
- 아무런 값을 넘겨주지 않으면 초기값은
undefined
- 상태가 아닌 변수를 사용해 상태값을 관리한다면?
- 리액트 렌더링은
- 함수 컴포넌트의 return과 클래스 컴포넌트의 render 함수를 실행한 다음
- 이 실행 결과를 이전 리액트 트리와 비교해 리렌더링이 필요한 부분만 업데이트해 이뤄진다
- 변수를 사용하는 건 위 리렌더링을 발생시키기 위한 조건을 충족 X
- 리액트 렌더링은
function Component() {
const [, triggerRender] = useState()
let state = 'hello'
const handleButtonClick = () => {
state = 'hi'
triggerRender()
}
return ...
}
- 리액트 렌더링은 함수 컴포넌트에서 반환된 결과물인 return의 값을 비교해 실행되기 때문이다.
- 즉, 매번 렌더링이 발생될 때마다 함수는 다시 새롭게 실행되고
- 새롭게 실행되는 함수에서 state는 매번 hello로 초기화되므로
- 아무리 state를 변경해도 다시 hello로 초기화
- 함수 컴포넌트는 매번 함수를 실행해 렌더링이 일어나고
- 함수 내부의 값은 함수가 실행될 때마다 다시 초기화
- 그렇다면 useState 훅의 결괏값은 어떻게 함수가 실행되어도 그 값을 유지하는 것?
const [value, setValue] = useState(0);
setValue(1);
console.log(value); // 0
- setValue로 값을 변경했음에도
- 훅 내부의 setState를 호출하더라도
- 변경된 새로운 값을 반환하지는 못한 것
- 그럼 state를 함수로 바꿔서 state의 값을 호출할 때마다 현재 state를 반환하게 하면 된다.
const [value, setValue] = useState(0);
setValue(1);
console.log(value()); // 1
- state는 상수처럼 사용하기 때문에 함수처럼 사용하는 것은 동떨어져 있다..
- ⭐️ 리액트에서는 클로저를 이용
- useState 내부에 선언된 함수 setState가 함수의 실행이 종료된 이후에도
- 지역변수인 state를 계속 참조할 수 있다는 것을 의미
- 클로저를 사용함으로써
- 해당 값을 노출 X → 오직 리액트에서만 쓸 수 있고
- 함수 컴포넌트가 매번 실행되더라도 useState에서 이전의 값을 정확하게 꺼내 쓸 수 있게 되는 것
게으른 초기화
- useState의 초기값을 선언하기 위해 원시값을 넣는 경우가 대부분
- useState의 인수로 특정한 값을 넘기는 함수를 인자로 넣는 것도 가능
- ⭐️ 함수를 인자로 넘기는 것을 **게으른 초기화(lazy initialization)**라고 한다.
// 일반적인 useState 사용
const [count, setCount] = useState(Number.parseInt(window.localStorage.getItem(cacheKey)));
// 게으른 초기화
const [count, setCount] = useState(() => Number.parseInt(window.localStorage.getItem(cacheKey)));
- ⭐️ 리액트 공식문서에서 게으른 초기화는 이럴 때..
- 초기값이 복잡하거나
- 무거운 연산을 포함하고 있을 때
- ⭐️ 게으른 초기화 함수는 오로지 state가 처음 만들어질 때만 사용된다.
- 리렌더링이 발생하면 이 함수의 실행은 무시
- 리액트에서 렌더링이 실행될 때마다
- 함수 컴포넌트의 함수가 다시 실행
- 즉, 함수 컴포넌트의 useState의 값도 재실행
- 클로저를 통해 값을 가져오는 것
- useState의 인수로 많은 비용을 요구하는 작업이 들어가 있다면
- 계속해서 실행될 위험
3.1.2 useEffect
- useEffect.. 꽤나 까다로운 자슥
- ⭐️ useEffect의 정의
- 애플리케이션 내 컴포넌트들의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 매커니즘
useEffect란?
- 첫 번째 인수로, 실행할 부수 효과가 포함된 함수
- 두 번째 인수로, 의존성 배열
- 의존성 배열이 변경될 때마다 useEffect의 첫 번째 인수인 콜백을 실행한다
🤔 useEffect는 의존성 배열이 변경된 것을 어떻게 알고 실행될까?
- 기억! 함수 컴포넌트는 매번 함수를 실행해 렌더링을 수행
- proxy나 바인딩, 옵저버 같은 기능으로 값의 변화를 관찰하는 것 X
- 렌더링할 때마다 의존성에 있는 값을 보면서
- 이 의존성의 값이 이전과 다른 게 하나라도 있으면
- 부수효과를 실행하는 평범한 함수
클린업 함수의 목적
- 클린업 함수라 불리는 useEffect 내에서 반환되는 함수는 정확히 무엇?
- ⭐️ 특정 이벤트의 핸들러가 무한히 추가되는 것을 방지
- ⭐️⭐️ 언마운트 개념과는 차이가 있다.
- 언마운트는 컴포넌트가 DOM에서 사라진다는 것의 의미하는 클래스 컴포넌트의 용어
- 클린업 함수는 언마운트라기보다
- 함수 컴포넌트가 리렌더링 되었을 때
- 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는
- 말 그대로 이전 상태를 청소해 주는 개념
의존성 배열
- 의존성 배열 인수 자리를 없이 useEffect를 쓴다면 렌더링될 때마다 useEffect가 실행
- 🤔 그럼 useEffect 없이 사용하는 것과 똑같이 동작하는 거 아닌가?
- ⭐️ useEffect는 클라이언트 사이드에서 실행되는 것을 보장
- ⭐️ useEffect는 렌더링이 완료된 이후에 실행된다.
- 반면 함수 중간에 쓰는 건 렌더링되는 도중에 실행된다.
- 따라서 서버 사이드에서도 실행된다.
- 즉, 함수 컴포넌트의 반환을 지연시키는 행위
- 즉, 무거운 작업일 경우 렌더링을 방해 → 성능에 악영향
- 🤔 그럼 useEffect 없이 사용하는 것과 똑같이 동작하는 거 아닌가?
useEffet를 사용할 때 주의할 점
-
eslint-disable-lint react-hooks/exhausive-deps 주석은 최대한 자제하라
- useEffect 인수 내부에서 사용하는 값 중 의존성 배열에 포함되어 있지 않는 값이 있을 때 경고 발생
- 보통 빈 배열 전달 === 마운트하는 시점에만 무언가를 하고 싶다
- 이는 클래스 컴포넌트의 생명주기 메서드인 componentDidMount에 기반한 접근법
- useEffect는 전달한 값의 변경에 의해 실행되어야 하는 훅
- 부수효과가 실제로 관찰해서 실행되어야 하는 값과는 별개로 작동한다는 것을 의미
- ⭐️ 정말 의존성으로
[]
이 필요하다면- 최초에 함수 컴포넌트가 마운트 되었을 시점에만 콜백 함수 실행이 필요한지를 다시 한번 체크
-
useEffect의 첫 번째 인수에 함수명을 부여하라
- useEffect의 첫 번째 인수로 익명 함수를 보통 넘기는데
- useEffect의 수가 적거나 복잡성이 낮다면 노상관
- useEffect의 코드가 복잡하고 많아질수록 무슨 일을 하는 useEffect 코드인지 파악하기 어려움
- 이럴 때 이름을 붙여보자
useEffect( const logActiveUser = () => { logging(user.id) }, [user.id] )
🤔 왜 useEffect의 콜백 인수로 비동기 함수를 바로 넣을 수 없을까?
- useEffect 내부에서 state를 결과에 따라 업데이트하는 로직이 있다고 가정
- useEffect의 인수로 비동기 함수가 사용 가능하다면
- 비동기 함수의 응답 속도에 따라 결과가 이상하게 나타날 수 있다.
- ⭐️ useEffect의 경쟁 상태 (race condition)
- 즉, 비동기 useEffect는 state의 경쟁 상태를 야기할 수 있고
- cleanup 함수의 실행 순서도 보장할 수 없기 때문에 개발자의 편의를 위해 인수로 받지 않는 것
3.1.3 useMemo
- 비용이 큰 연산에 대한 결과를 저장해 두고, 이 저장된 값을 반환하는 훅
- 첫 번째 인수로, 어떠한 값을 반환하는 생성함수
- 두 번째 인수로, 해당 함수가 의존하는 값의 배열을 전달
- 단순히 값뿐만 아니라 컴포넌트도 가능
const MemoizedComponent = useMemo(() => ((<ExpensiveComponent value={value} />), [value]));
- useMemo를 사용하면, 무거운 연산을 다시 수행하는 것을 막을 수 있다
- 어떠한 값을 계산할 때 해당 값을 연산하는 데 많은 비용이 든다면 사용해 봄 직
3.1.4 useCallback
- 인수로 넘겨받은 콜백 자체를 기억
- 즉, 특정 함수를 새로 만들지 않고 다시 재사용한다는 의미
다음은 memo를 사용함에도 전체 자식 컴포넌트가 리렌더링되는 예제이다.
const ChildComponent = memo((props) => {
const { name, value, onChange } = props
// 렌더링이 수행되는지 확인하기 위한 용도
useEffect(() => {
console.log('rendering', name)
})
return (
<>
<h1>
{name} {value ? '켜짐' : '꺼짐'}
</h1>
<buton onClick={onChange}>toggle</button>
</>
)
})
function App() {
const [status1, setStatus1] = useState(false)
const [status2, setStatus2] = useState(false)
const toggle1 = () => {
setStatus1(!status1)
}
const toggle2 = () => {
setStatus2(!status2)
}
return (
<>
<ChildComponent name="1" value={status1} onChange={toggle1} />
<ChildComponent name="2" value={status2} onChange={toggle2} />
</>
)
}
- memo를 사용했지만 App의 자식 컴포넌트 전체가 렌더링되고 있다.
- ⭐️ 이유!!
- 렌더링되고, 그때마다 매번 onChange 함수가 재생성되고 있어서
- 따라서, 함수의 메모이제이션을 위해 useCallback 사용
3.1.5 useRef
- useState와 동일하게
- 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점
- ⭐️ useState와는 구별되는 차이점?
- useRef는 반환값인 객체 내부에 있는
current
로 값에 접근 또는 변경할 수 있다. - useRef는 그 값이 변하더라도 렌더링을 발생 X
- useRef는 반환값인 객체 내부에 있는
- 🤔 useRef는 왜 필요한걸까?
- 렌더링에 영향을 미치지 않는다면, 그냥 함수 외부에 선언해서 관리하는 것도 동일한 기능?
결론부터.. 위 방식은 몇 가지 단점 존재let value = 0; function Component() { const handleClick = () => { value += 1; }; }
- 컴포넌트가 실행되어 렌더링되지 않았음에도 value라는 값이 기본적으로 존재
- 이는 메모리에 불필요한 값을 갖게 하는 악영향
- 만약!!! Component, 즉 컴포넌트가 여러 번 생성된다면
- 각 컴포넌트에서 가리키는 값이 모두 value로 동일하다
- 컴포넌트가 초기화되는 지점이 다르더라도
- 컴포넌트 인스턴스 하나당 하나의 값을 가질 수가 없다.
- useRef는 언급한 문제를 모두 극복할 수 있는 리액트식 접근
- ⭐️ 컴포넌트가 렌더링될 때만 생성 & 인스턴스가 여러 개라도 각각 별개의 값을 바라본다.
- useRef의 대표적인 예: DOM에 접근하고 싶을 때
function RefComponent() { const inputRef = useRef(); // 이때는 미처 렌더링이 실행되기 전(반환되기 전)이므로 undefined를 반환 console.log(inputRef.current); // undefined useEffect(() => { console.log(inputRef.current); // <input type="text"></input> }, [inputRef]); return <input ref={inputRef} type="text" />; }
3.1.6 useContext
Context란?
- 리액트 애플리케이션은 기본적으로 부모 컴포넌트와 자식 컴포넌트로 이뤄진 트리 구조
- 부모가 가지고 있는 데이터를 자식에서도 사용하고 싶다면 props로 데이터를 넘겨주는 것이 일반적
- props drilling 조심!
- props 내려주기는 데이터를 제공하는 쪽이나 사용하는 쪽 모두에게 불편
- 이러한 불편함을 극복하기 위해 등장한 것이 Context
Context를 함수 컴포넌트에서 사용할 수 있게 해주는 useContext 훅
- useContext를 사용하면 상위 컴포넌트 어딘가에서 선언된
<Context.Provider />
에서 제공한 값을 사용 가능 - 만약 여러 개의 Provider가 있다면 가장 가까운 Provider의 값을 가져오게 된다.
useContext를 사용할 때 주의할 점
- useContext가 선언되어 있다 === Provider에 의존성을 가지고 있는 것
- 즉, 아무데서나 재활용하기에는 어려운 컴포넌트가 된다.
- 상위에 Provider가 없으면 의도대로 동작하지 않을 수 있기 때문
- 이러한 상황을 방지하려면
- useContext를 사용하는 컴포넌트를 최대한 작게 하거나
- 혹은 재사용되지 않을 만한 컴포넌트에서 사용
- ⭐️ useContext는 상태 관리를 위한 API가 아니다.
- 상태를 주입해 주는 API
- 상태 관리 라이브러리가 되기 위해서는 최소한 다음 2가지 조건에 만족
- 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
- 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 한다.
- 컨텍스트는 둘 다 못한다.
3.1.8 useImperativeHandle
- 자주 볼 수는 없지만 일부 사용 사례에서 유용하게 활용 가능
- 이 훅을 이해하기 위해서는 forwardRef에 대해 알아야 한다.
forwardRef 살펴보기
- ref는 useRef에서 반환한 객체
- 리액트 컴포넌트의 props인 ref에 넣어 HTMLElement에 접근하는 용도로 흔히 사용
- key와 마찬가지로 ref도 리액트에서 컴포넌트의 props로 사용할 수 있는 예약어
- ref를 상위 컴포넌트에서 하위 컴포넌트로 전달하고 싶다면?
- 상위 컴포넌트에서는 접근하고 싶은 ref가 있지만
- 이를 직접 props로 넣어 사용할 수 없을 때는 어떻게 해야 할까?
- ref는 props로 사용할 수 없다는 경고문
- 대신 다른 props명을 통해 ref를 전달하는 것은 가능
- ⭐️ forwardRef가 탄생한 배경
- ref를 전달하는 데 있어서 일관성을 제공하기 위해
- 어떤 props명으로 전달할지 모르고
- 이에 대한 완전한 네이밍의 자유가 주어진 props보다는
- forwardRef를 사용하면 좀 더 확실하게 ref를 전달할 것임을 예측 가능
- 또 사용하는 쪽에서도 확실히 안정적으로 받아서 사용 가능
const ChildComponent = forwardRef((props, ref) => {
useEffect(() => {
console.log(ref);
}, [ref]);
return <div>Hello</div>;
});
const ParentComponent = () => {
const inputRef = useRef();
return (
<>
<input ref={inputRef} />
<ChildComponent ref={inputRef} />
</>
);
};
useImperativeHandle이란?
- 부모에게서 넘겨받은 ref를 원하는대로 수정할 수 있는 훅
const Input = forwardRef((props, ref) => {
// useImperativeHandle을 사용하면 ref의 동작을 추가로 정의할 수 있다.
useImperativeHandle(
ref,
() => ({
alert: () => alert(props.value),
}),
[props.value],
);
return <input ref={ref} {...props} />;
});
const App = () => {
// input에 사용할 ref
const inputRefv = useRef();
// input의 value
const [text, setText] = useState('');
const handleClick = () => {
// inputRef에 추가한 alert라는 동작을 사용할 수 있다.
inputRef.current.alert();
};
const handleChange = (e) => {
setText(e.target.value);
};
return (
<>
<Input ref={inputRef} value={text} onChange={handleChange} />
<button onClick={handleClick}>Focus</button>
</>
);
};
- 부모 컴포넌트에서 노출되는 값을 원하는대로 바꿀 수 있다
- useImperativeHandle을 사용하면 ref의 값에 원하는 값이나 액션을 정의 가능
3.1.9 useLayoutEffect
- ⭐️ 공식문서에서는..
- 이 함수의 시그니처는 useEffect와 동일하나, 모든 DOM의 ㅂ녀경 후에 동기적으로 발생한다.
- 시그니처가 동일하다 === 사용 방법이 동일하다
- ⭐️ 모든 DOM의 변경 후에 useLayoutEffect의 콜백 함수 실행이 동기적으로 발생
- 실행순서
- 리액트가 DOM을 업데이트
- useLayoutEffect를 실행
- 브라우저에 변경 사항을 반영
- useEffect를 실행
- ⭐️ useLayoutEffect가 useEffect보다는 먼저 실행
- useLayoutEffect는 브라우저에 변경 사항이 반영되기 전에 실행
- 반면 useEffect는 브라우저에 변경 사항이 반영된 이후에 실행
- ⭐️ 동기적이라는 것은
- 리액트는 useLayoutEffect의 실행이 종료될 때까지 기다린 다음에 화면을 그린다.
- 즉, 리액트 컴포넌트는 useLayoutEffect가 완료될 때까지 기다림
- 그래서 컴포넌트가 잠시동안 일시 중지되는 것과 같은 일이 발생하기도
- 🤔 useLayoutEffect를 언제 사용?
- DOM은 계산되었지만, 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때
- 반드시 필요할 때만 사용하는 것이 좋다.
3.1.11 훅의 규칙
- 리액트에서 제공하는 훅은 사용하는 데 몇 가지 규칙 존재
- 최상위에서만 훅을 호출해야 한다.
- 반복문이나, 조건문, 중첩된 함수 내에서 훅 실행 X
- 이 규칙을 따라야만 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅 호출 보장 가능
- 훅을 호출할 수 있는 것은 리액트 함수 컴포넌트 혹은 사용자 정의 훅 딱 2가지 경우
- 일반 자바스크립트 함수에서는 훅 사용 X
- 최상위에서만 훅을 호출해야 한다.