1. 탐구
1. useState
: 함수 형 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅
import { useState } from 'react'
const [state,setState] = useState();
useState의 인수로는 사용할 state의 초기값을 넘겨준다. 아무런 값을 넘기지 않으면 초기값은 undefined이다. useState훅의 반환 값은 배열이며, 배열의 첫 번째 원소로 state값 자체를 사용할 수 있고, 두 번째 원소인 setState함수를 사용해 해당 state의 값을 변경할 수 있다.
함수형 컴포넌트는 매번 함수를 실행해 렌더링이 일어나고, 함수 내부의 값은 함수가 실행될 때마다 다시 초기화된다. 그럼에도 useState 훅의 결과값은 함수가 실행되도 그 값을 유지하는 이유는 closure를 이용했기 때문이다.
게으른 초기화
: useState에 변수 대신 함수를 넘기는 것
useState는 초기값으로 초기화를 단 한 번만 실행하기 때문에, 이 게으른 초기화 함수는 오로지 state가 처음 만들어질 때만 사용된다.
무거운 연산이 요구될 때 사용을 권장한다.
- localStorage, sessionStorage에 대한 접근
- map, filter, find같은 배열에 대한 접근
- 초기값 계산을 위해 함수 호출이 필요할 때
2. useEffect
: state와 props의 변화 속에서 일어나는 렌더링 과정에서 실행되는 부수 효과 함수
- 애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수효과를 만드는 매커니즘
클린업 함수
: useEffect는 콜백이 실행될 때마다 이전의 클린업 함수가 존재한다면 그 클린업 함수를 실행한 뒤에 콜백을 실행한다. 이렇게 함으로써 특정 이벤트의 핸들러가 무한히 추가되는 것을 방지할 수 있다.
언마운트라기보다는 함수형 컴포넌트가 리렌더링되었을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는, 말 그대로 이전 상태를 청소해주는 개념으로 보면 된다.
의존성 배열
- 빈 배열 : 최초 렌더링 직후에 실행된 다음부터는 더 이상 실행하지 않는다.
- 아무런 값도 넘겨주지 않는다면(콜백함수만 넘겨줌) : 렌더링이 발생할 때마다 실행된다.
- useEffect를 사용하지 않을 때와 차이점 : 클라이언트 사이드에서 실행되는 것을 보장해준다. 즉, window객체의 접근에 의존하는 코드를 사용해도 된다.
리액트는 useEffect를 포함하여, 값을 비교할 때 Object.is를 기반으로 하는 얕은 비교를 수행한다.
useEffect 사용 시 주의할 점
eslint-disable-line react-Hooks/exhaustive-deps주석을 최대한 자제하라- 정말로 필요한 때에는 사용할 수 있지만 대부분의 경우에는 의도치 못한 버그를 만들 가능성이 큰 코드이다.
- useEffect에 빈 배열을 넘기기 전에는 정말로 useEffect의 부수효과가 컴퓨넌트의 상태와 별개로 작동해야만 하는지, 혹은 여기서 호출하는 게 최선인지 한 번 더 검토해봐야 한다.
-
useEffect의 첫 번째 인수에 함수명을 설정하라
useEffect( function logActiveUser(){ loggin(user.id) }, [user.id], ) -
거대한
useEffect를 만들지 마라- 가능한 한
useEffect는 간결하고 가볍게 유지하는 것이 좋다.
- 가능한 한
- 의존성 배열에 불가피하게 여러 변수가 들어가야 하는 상황이라면 최대한
useCallback과useMEmo등으로 사전에 정제된 내용들만 useEffect에 담아두는 것이 좋다.
- 불필요한 외부 함수를 만들지 마라
- useEffect 내에서 사용할 부수효과라면 내부에서 만들어서 정의해서 사용하는 편이 훨씬 도움이 된다.
- useEffect 내부에서 비동기 함수를 첫 번째 인자로 지정할 수 없다.
- useEffect 내부에서 비동기 함수를 선언해 실행하거나, 즉시 비동기 실행 함수를 만들어서 사용하는 것은 가능하다.
Quiz
import { useEffect } from "react";
export default function Parant() {
useEffect(() => {
console.log("parent");
});
return <Child />;
}
function Child() {
useEffect(() => {
console.log("child");
});
return <GrandChild />;
}
function GrandChild() {
useEffect(() => {
console.log("grandchild");
});
return <div></div>;
}
-
위 코드의 결과는?
- React는 부모 컴포넌트부터 자식 컴포넌트 순으로 렌더링한다. 하지만 useEffect는 브라우저가 그려진 후에 실행된다. 따라서,
useEffect는 자식 컴포넌트에서 부모 컴포넌트 순으로 실행된다.
- React는 부모 컴포넌트부터 자식 컴포넌트 순으로 렌더링한다. 하지만 useEffect는 브라우저가 그려진 후에 실행된다. 따라서,
3. useMemo
: 비용이 큰 연산에 대한 결과를 저장(메모이제이션)해 두고, 이 저장된 값을 반환하는 훅
- 렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면 함수를 재실행하지 않고 이전에 기억해 둔 해당 값을 반환하고
- 의존성 배열의 값이 변경됐다면 첫 번째 인수의 함수를 실행한 후에 그 값을 반환하고 그 값을 다시 기억해둘 것이다.
4. useCallback
: 인수로 넘겨받은 함수 자체를 기억한다.
useMemo와 useCallback의 유일한 차이는 메모이제이션을 하는 대상이 변수냐 함수냐일 뿐이다.
5. useRef
ref
: 생성된 DOM노드나 React 엘리먼트에 접근하는 방법을 제공
ref를 사용하는 경우
focus, 텍스트 선택 영역, 혹은 미디어의 재생 관리할 때- 애니메이션을 직접적으로 실행시킬 때
- 서드 파티 DOM 라이브러리를 react와 같이 실행할 때
- 값이 변경되어도 컴포넌트가 리렌더링되지 않도록 하기 위한 값을 저장하기 위해
useRef를 사용하지 않고 컴포넌트 바깥에서 변수를 전역으로 선언할 경우
- 컴포넌트가 실행되어 렌더링되지 않았음에도 value라는 값이 기본적으로 존재하게 된다.
- 이는 메모리에 불필요한 값을 갖게 하는 악영향을 미친다.
- 컴포넌트가 여러 번 생성된다면 각 컴포넌트에서 가리키는 값이 모두 value로 동일하다.
Ref 특징
- state와 차이점 : reactivity
- state와 달리 React의 렌더링을 트리거하지 않음
- props로 ref를 할당하면 해당 변수에 DOM 객체가 자동으로 바인딩 된다. 이 방식은 React가 생성하는 DOM객체를 별도의 DOM query없이 손쉽게 접근할 수 있게 해준다.
ref의 선언과 할당
- 함수형 컴포넌트에서는
useRef를 이용하여 선언하고 초기화 - ref에 할당하는 값은 자동으로
current라는 변수에 저장되므로, 항상 참조할 떄current값의 존재 여부를 확인하고 사용해야 함- 안전하게 사용하기 위해서는 렌더링 및 마운트 된 이후인
useEffect안에서 사용
- 안전하게 사용하기 위해서는 렌더링 및 마운트 된 이후인
- ref는 state와 달리 계산되는 시점에만 유효한 값
Ref의 활용
ref는 단지 특정 시점에만 유효하다는 점 말고는 그저 값을 보관하고 있는 변수에 지나지 않는다. 또한, reactivity를 가지지 않기 떄문에 값을 설정해도 렌더링이 일어나지 않는다. 이를 어떻게 활용할 수 있을까?
-
useMountHook마운트 후에 단 1번만 실행되는 Effect를 만들고 싶은 경우 ref를 활용하면 된다.
- ref는 current객체를 가진다.
- ref는 DOM이 마운트된 이후에 실행이 된다.
이러한 ref의 두 가지 특성을 이용한 것이다.
// useMount
function useMount() {
const ref = useRef(false);
useEffect(() => {
if (!ref.current) {
ref.current = true;
}
}, []);
return { isMount: ref.current }
}
- React 18버전부터 development환경의 strict모드에서는 mount 시점에 useEffect가 항상 2번 실행된다. 이 동작으로 인해 사이드이펙트가 발생할 수 있다.
- 컴포넌트에 ref 전달하기
- forwardRef : ref를 컴포넌트에 전달하기 위해 사용
- 예시
const Label = React.forwardRef<LabelElement, LabelProps>((props, forwardedRef) => {
return (
<Primitive.label
// ...
ref={forwardedRef}
/>
);
})
💡
ref가 아닌React.forwardRef를 사용한 이유?
- React에서
ref는 컴포넌트에 대한 직접적인 접근을 가능하게하는 수단이다. DOM 요소에 대한 참조를 보관할 수 있다. 함수형 컴포넌트는 자체적으로 인스턴스를 가지지 않기 때문에ref를 직접 사용할 수 없다. 대신에 React는React.ForwardRef라는 API를 제공하여 함수형 컴포넌트에서ref를 받아서 내부의 DOM요소나 다른 클래스 컴포넌트에 전달(forward)할 수 있도록 한다.- 위 코드에서는
React.forwardRef를 사용하여label컴포넌트에서ref를 받아서Primitive.label컴포넌트에 전달하고 있다. 이렇게 하면Label컴포넌트를 사용하는 곳에서ref를 사용할 수 있게 된다.- 이를 통해 예를 들어, 해당 DOM 요소에 포커스를 주거나, 해당 요소의 크기나 위치 정보를 가져오는 등의 작업을 수행할 수 있다.
Label컴포넌트의 활용
import * as React from 'react';
import { Label, LabelProps } from './Label';
const ColorChangingLabel: React.FC<LabelProps> = (props) => {
const labelRef = React.useRef<HTMLLabelElement>(null);
const handleClick = () => {
if (labelRef.current) {
labelRef.current.style.color = labelRef.current.style.color === 'red' ? 'black' : 'red';
}
};
return <Label {...props} ref={labelRef} onClick={handleClick} />;
};
export default ColorChangingLabel;
Label컴포넌트에labelRef를Label에 넘겨줌으로써 컴포넌트 바깥에서도label의 DOM요소에 접근할 수 있게 되었다.- 이를 통해
label의style을 변경하는 코드를 작성하였다.
- ref 커스터마이징 하기
useImperativeHandle: ref에 할당되는 값을 DOM객체가 아닌 컴포넌트 내부에서 커스터마이징한 객체로 변경할 수 있음
- 즉, 자식 컴포넌트에서 노출하고 싶은 ref 객체를 따로 지정 가능
function ChildComponent(props, ref) {
useImperativeHandle(ref, () => {
return {
getText: () => 'useImperativeHandle 테스트'
};
}, []);
return <span>children ref 테스트</span>
}
const ForwardedChild = forwardRef(ChildComponent);
function ParentComponent() {
const childRef = useRef(null);
useEffect(() => {
console.log(childRef.current?.getText());// 'useImperativeHandle 테스트'
}, []);
return (
<div>
<ForwardedChild ref={childRef} />
</div>
);
}
useRef와 Typescript
-
Type 선언하지 않을 경우 발생하는 타입 에러
-
아래와 같이 타입을 선언해주면 된다.
interface MutableRefObject<T> { current: T; } interface RefObject<T> { readonly current: T | null; }
useRef 타입 분해해보기
@types>react>index.d.ts를 살펴보면 아래의 3개의 타입이 오버로딩되어 있다. 이 타입들을 한 번 살펴볼까?
1. useRef<T>(initialValue: T): MutableRefObject<T>;
- 인자의 타입과 제네릭의 타입이 T로 일치하는 경우
MutableRefObject<T>: 위의 정의에서 볼 수 있듯이current프로퍼티 그 자체를 직접 변경할 수 있음
2. useRef<T>(initialValue: T|null):RefObject<T>;
- 인자의 타입이 null을 허용하는 경우
- RefObject
: 위의 정의에서 볼 수 있듯이 curent프로퍼티를 직접 수정할 수 없음 (readonly)
3. useRef<T = undefined() : MutableRefObject<T | undefined>;
- 제네릭의 타입이
undefined인 경우(타입을 제공하지 않은 경우), **MutableRefObject<T | undefined>**를 반환
인자의 타입 = 제네릭 타입이 일치하는 경우 1번, 인자의 타입이 null인 경우 2번, 타입이 제공되지 않을 경우 3번의 useRef타입이 적용이 된다.
-
로컬 변수 용도로
useRef사용 : 1번의 MutableRefObject를 사용해야 하므로 제네릭 타입과 같은 타입의 초기값을 넣어주기 const localVarRef = useRef<number>(0); -
DOM을 직접 조작 : 2번의 RefObject
를 사용해야 하므로 초기값으로 null 넣어주기 const inputRef = useRef<HTMLInputElement>(null);
ref를 손자 컴포넌트에게 넘겨주기
ref를 자식 컴포넌트에게 전달하기 위해서는 forwardRef를 이용한다.
-
예시
const FancyButton = React.forwardRef((props, ref) => ( <button ref={ref} className="FancyButton"> {props.children} </button> )); // You can now get a ref directly to the DOM button: const ref = React.createRef(); <FancyButton ref={ref}>Click me!</FancyButton>;
자식 컴포넌트에게 ref를 전달하기 위해서는 위와 같이 사용하면 된다. 그렇다면, 손자 컴포넌트에게 전달하고 싶은 경우 어떻게 해야할까?
-
부모로부터 전달받은
ref를 그대로 자식에게 전달해주면 된다.import { forwardRef, useRef, useEffect } from "react"; export function Parent() { const elementRef = useRef() useEffect(() => { console.log(elementRef.current); // logs <div>Deep!</div> }, []) return <Child ref={elementRef} /> } const Child = forwardRef(function (props, ref) { return <GrandChild ref={ref} /> }) const GrandChild = forwardRef(function (props, ref) { return <div ref={ref}>Deep!</div> })
6. useContext
: Context를 함수형 컴포넌트에서 사용할 수 있게 해주는 훅
- Context : 명시적인 props전달 없이도 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있따.
- props drilling을 극복하기 위해 등장
예시 코드
import React, { createContext, useContext, useEffect, useState } from "react";
const MyContext = createContext(undefined);
function ContextProvider({ children, text }) {
return (
<MyContext.Provider value={{ hello: text }}>{children}</MyContext.Provider>
);
}
function useMyContext() {
const context = useContext(MyContext);
if (context === undefined) {
throw new Error(
"useContext는 ContextProvider 내부에서만 사용할 수 있습니다."
);
}
return context;
}
export default function Parant() {
useEffect(() => {
console.log("parent");
});
const [text, setText] = useState("");
const handleChangeInput = (e) => {
setText(e.target.value);
};
return (
<ContextProvider>
<Child />
<input type="text" value={text} onChange={handleChangeInput} />
</ContextProvider>
);
}
const Child = function Child() {
useEffect(() => {
console.log("child");
});
return <GrandChild />;
};
const GrandChild = function GrandChild() {
useEffect(() => {
console.log("grandchild");
});
const { hello } = useMyContext;
return <div>{hello}</div>;
};
-
주의점 : React.memo는 선언형 함수로 선언한 함수에는 사용하지 못한다. 위처럼 변수에 할당을 해서 React.memo로 감싸야 한다.
-
GrandChildren을
React.memo로 감싸지 않았을 때
-
GrandChildren을
React.memo로 감싸서 메모이제이션을 했을 때 : 컴포넌트의 props가 바뀌지 않으면 리렌더링이 일어나지 않는다.
메서드/사용법
createContext: Context를 만든다Context.Provider: 1번에서 만든 Context를 각 컴포넌트에 뿌려주는 Provider이다.useContext(Context): 만든 Context를 불러와 사용할 수 있다.- 이 때, 1번에서 만든 Context를 인자로 넘긴다.
- 2번에서 만든 Context.Provider로 감싸있지 않다면, 에러가 발생하도록 분기처리를 해주면 좋다
주의할 점
useContext는 상태 관리를 위한 리액트의 API가 아니라, 엄밀히 따지면 상태를 주입해주는 API이다. 상태 관리 라이브러리를 위한 두 가지 조건을 모두 충족하지 않는다.
- 어떠한 상태를 기반으로 다른 상태를 만들어낼 수 있다.
- 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 한다.
7. useReducer
: useState의 심화 버전으로, 좀 더 복잡한 상태값을 미리 정의해놓은 시나리오에 따라 관리할 수 있다.
반환값
- state : 현재 useReducer가 가지고 있는 값
- dispatcher
인수
- reducer
- initialState
- init
2. 실습
1. useRef
1. useRef 사용
function App() {
const ref = useRef(1);
console.log(ref); // { current : 1 }
return <div className="App"></div>;
}
export default App;
- ref는
{current : intialValue}를 가지고 있는 객체이다.
2. useRef값을 변경한다.
function App() {
const ref = useRef(1);
console.log(ref); // { current : 1 }
ref.current++;
console.log(ref); // { current : 2 }
return (
<div className="App">
{ref.current}
<button
onClick={() => {
ref.current++;
console.log(ref.current);
}}
>
Click
</button>
</div>
);
}
export default App;
-
{ref.current}의 값은 화면 상에서 2로 출력이 된다. 왜냐하면, 컴포넌트가 처음 렌더링 될 때는 JS 코드 > XML 코드 순으로 동기적으로 읽기에, ref.current값의 변경이 반영이 된다.
-
button 클릭 시, console에는 current.ref의 값이 1씩 증가한다.
하지만, 화면의 값은 2 그대로이다. 왜냐하면, ref 값의 변화는 리렌더링을 일으키지 않기 때문이다.
3. ref가 아닌 다른 state을 변경한다.
만약, 컴포넌트 자체에 리렌더링이 일어나면, 화면 상의 ref.current값이 변경될까?
function App() {
const ref = useRef(1);
const [count, setCount] = useState(0);
console.log(ref);
ref.current++;
console.log(ref);
return (
<div className="App">
{ref.current}
<button
onClick={() => {
ref.current++;
setCount(count + 1);
console.log(ref.current);
}}
>
Click
</button>
</div>
);
}
export default App;
그렇다. state의 변경은, 컴포넌트 자체의 리렌더링을 일으키고, 이는 ref.current의 변화를 반영하는 것을 의미한다.
위의 결과는 어떠할까?
화면 상에서 값은 2씩 증가한다. 왜냐하면, XML코드 바깥에서도 ref.current++, 버튼 클릭시에도 ref.current++를 수행하기 때문이다.
4. setTimeout의 값을 보존
export default function App(){
const timerRef = useRef(null);
function handleSendClick = () => {
setIsLoading(true);
timerRef.current = setTimeout(()=>{
alert("sent!");
setIsLoading(false);
},3000))
}
function handleUndoClick = () => {
clearTimeout(timerRef.current); // 이 부분
}
}
이 부분에서, 일반 변수가 아닌 ref에 저장을 함으로써 컴포넌트가 리렌더링이 일어나도 react는 기존 값을 보존하고 있는다.
5. 컴포넌트 바깥에서의 데이터 보존
컴포넌트 내부에서 선언한 일반 값은, 리렌더링이 일어날 때 다시 해당 코드가 실행되어 값이 초기화된다. 하지만, 컴포넌트 바깥에서 선언한 일반 값은 컴포넌트 리렌더링이 일어나도 값이 보존된다.
let a = 1;
export default function App() {
let b = 1;
const ref = useRef(1);
console.log(a, b, ref.current);
const [state, setState] = useState(1);
return (
<div className="App">
{ref.current}
<button
onClick={() => {
a++;
b++;
ref.current++;
setState(state + 1);
}}
>
Click
</button>
</div>
);
}
- 가운데값(컴포넌트 내부에서 선언한 값)만 1로만 출력이 되고, 그 외의 값은 1씩 증가한다.
- 컴포넌트 내부에서 선언한 값은 컴포넌트가 리렌더링이 일어날 때마다 선언문이 계속 실행되기 때문이다.
ref는 current가 아닌 다른 key값에도 변수를 저장할 수도 있다.
function App() {
const ref = useRef(null);
const handleButton = () => {
ref.a = ref.a ? ref.a + 1 : 1;
console.log(ref);
};
return (
<div className="App">
<div ref={ref} />
<button onClick={handleButton}>버튼</button>
</div>
);
}