준영
훅
- 함수 컴포넌트가 상태를 사용하거나 클래스 컴포넌트의 생명주기 메서드를 대체하는 등 다양한 작업을 하기 위한 장치
- 클래스 컴포넌트에서만 가능했던 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
- 최상위에서만 훅을 호출해야 한다.