잘하고 싶다는 욕심을 가지는 건 좋은 일이지만
남과 비교하는 건 틀린 방향을 보고 있는 거라는 거 기억하기🧟♀️
useState는 클로저를 통해서 관련된 정보를 저장해두고 필요할 때마다 꺼내서 사용할 수 있게 한다
state는 useState 외부에 저장되며, useState는 클로저를 이용해 외부에 있는 state에 대한 정보를 useState 내부에서 접근하여 기억한다.
//리액트 모듈이 있다고 할 때
const React = (function (){
//global 내의 states에서 특정 객체에 상태들이 저장된다
const global = {};
//index는 global.states 에서 특정 상태값을 구분하기 위한 key로 사용된다
let index = 0;
function useState(initialState){
//key 값으로 global 객체에서 상태 값이 존재하는지 확인하고, 없는 경우에는 initial State를 할당한다
const currentState = global.states.[index] || initialState
const setState = (function (){
//unique key에 대한 정보를 클로저로 setState 내에서 가지고 있어 계속해서 동일한 index에 접근 가능함
const currentKey = index
return function (value) {
global.states[currentKey] = value
}
})()
//useState 사용 시 마다 indexfmf. +1 하고, setState 내에서 사용된다
//하나의 state마다 index가 배정되어 있게 됨
index = index + 1
return [currentState, setState];
}
//useState 함수 끝
}
useState의 초기값을 넘길 때 계산에 시간이 걸리는 변수 대신 함수 형태로 작성하면 리렌더링 시에는 다시 실행되지 않는다
//아래 대신
Number.parseInt(window.localStorage.getItem(cacheKey))
//함수로 useState 초기값 설정
() => Number.parseInt(window.localStorage.getItem(cacheKey))
위와 같이 작성하게 되면 초기값이 존재해 더 이상 초기값 계산이 필요없는 리렌더링 시에 계속해서 해당 값을 계산하는 낭비를 줄일 수 있게 됨
addEventListener와 같은 함수를 useEffect에서 실행할 때 클린업 함수에서는 removeEventListener를 해줄 수 있는데
useEffect는 콜백이 실행될 때마다 이전의 클린업 함수가 존재하면 클린업 함수를 실행한 뒤에 콜백을 실행함으로써 특정 이벤트의 핸들러가 무한하게 추가되는 것을 방지할 수 있다
(*vue에서 사용하던 unmount함수와는 좀 다른 느낌으로,
언마운트 - 특정 컴포넌트가 DOM에서 사라지는 것을 의미하나
클린업 함수 - 함수형 컴포넌트가 리렌더링 되었을 때 의존성 변화가 있었을 당시 이전 값을 기준으로 실행되는, 이전 값을 청소해주는 개념)
const React = (function (){
//useState와 마찬가지로 closer를 사용하므로 외부 객체에 저장하며, 식별에 index와 같은 key를 사용한다
const global = {};
let index = 0;
function useEffect (callback, dependencies){
const hooks = global.hooks
let prevDependencies = hook[index]
//이전 값이 있으면 이전 값을 얕은 비교로 비교해 변경이 일어났는지 확인하고, 최초 실행인 경우에는 변경이 일어난 것으로 간주
let isDependenciesChanged = prevDependencies ? dependencies.some((value, idx) => !Object.is(value, prevDependencies[idx]),) : ture
//변경이 일어났다면 콜백을 실행한다
if(isDependenciesChanged){
callback()
}
//현재 의존성을 훅에 다시 저장
hooks[index] = dependencies
//다음 훅이 일어날 때를 대비하기 위해 index 추가
index++
}
return {useEffect}
})()
useEffect 내부에서는 콜백의 인수로 비동기 함수를 바로 넣을 수 없는데, 비동기 함수의 응답 속도에 따라 결과가 이상하게 나오는 '경쟁 상태'가 발생할 수 있기 때문
비동기 함수를 콜백에 바로 넣으면 아래와 같은 에러가 발생한다
effect callbacks are synchronous to prevent race conditions. put the async function inside
비동기 함수를 콜백함수 내부에 넣어서 사용하면 이런 문제를 해결할 수 있음
비동기 함수 자체를 사용하는 게 문제는 아니고, useEffect의 인수로 비동기 함수를 지정할 수 없는 것임
다만 useEffect 내에 비동기 함수가 존재하면 내부에서 비동기 함수가 생성, 실행되는 것을 반복하므로 클린업 함수에서 이전 비동기 함수에 대한 처리를 추가해주는 게 좋은데,
fetch의 경우에는 abortController로 이전 요청을 취소하는 등의 처리를 할 수 있다
(javascript abortController
https://developer.mozilla.org/ko/docs/Web/API/AbortController
https://ko.javascript.info/fetch-abort
abortController 관련 번역 아티클
https://velog.io/@sehyunny/abort-controller-is-your-friend)
function Component({log}:{log:string}){
useEffect(() => {
logging(log)
},[})
}
useEffect(function logActiveUser(){
logging(user.id)
}, [user.id])
최근에 렌더링 관련해서 검색을 하다가 useRef는 값을 변경해도 렌더링을 발생시키지 않는다 ~ 와 같은 문장을 본 적 있다
렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있으므로
useState의 이전 값을 저장하는 usePrevious와 같은 훅을 구현하는 데 사용 가능
function usePrevious (value) {
const ref = useRef()
useEffect(() => {
ref.current = value
},[value])
return ref.current
}
function Component(){
const [counter, setCounter] = useState(0);
const previousCounter = usePrevious(counter);
function handleClick(){
setCounter((prev)=>prev+1)
}
return (
<button onClick={handleClick}>
{counter} {previousCounter}
</button>
)
컴포넌트가 실행될 때 해당 Context가 존재하는지 검사할 필요가 있을 때 아래 useMyContext와 같은 훅을 작성해 검사할 수 있다
특히 타입스크립트 사용 시에 타입 추론 등이 가능하므로 유용하게 사용할 수 있다
const MyContext = createContext({value:string} | undefined)
function ContextProvider({
children, text}:{PropsWithChildren<{text:string}>){
return (
<MyContext.Provider value={{value:text}}>
{children}
</MyContext.Provider>
)
}
function useMyContext (){
const context = useContext(MyContext);
if(context === undefined){
throw new Error(
'useMyContext는 ContextProvider 내부에서만 사용할 수 있습니다.')
}
return context
}
//사용
funciton ChildComponent(){
//훅 내부에서 타입이 명확하게 설정되어 있으므로 굳이 undefined 체크를 하지 않아도 된다
//ChildComponent가 Provider 하위에 없다면 에러가 발생함
const { value } = useMyContext();
return <>{value}</>
useContext를 사용하면 컴포넌트를 재활용하기 어려워진다는 점을 염두에 두어야 함
Provider에 의존성을 두게 되므로 아무데서나 재활용하기 어렵다
상태를 주입해주는 API라고 책에서는 표현했는데,
상태 관리 라이브러리가 되기 위한 아래 조건을 충족하지 않는다
단순히 props를 하위로 전달해줄 뿐, useContext를 사용한다고 해서 렌더링이 최적화 되지 않는다
예를 들어 A 컴포넌트에 Provider가 씌워져 있고, 하위의 B 컴포넌트, 그리고 B의 하위인 C 컴포넌트가 있을 때
C 컴포넌트에서만 Context에서 저장된 값을 사용한다고 해도, A에서 해당 값이 변화되면 B와 C 모두 리렌더링 된다
(여기서 최적화가 필요하다면 B 컴포넌트에 memo를 사용할 수 있음)
reducer라는 말은 개발에서 '뭔가를 변경하는 것' 정도의 의미로 사용되는 것 같다
처음에는 reduce가 줄이다의 의미이니까 줄이게 하는 거...? 하면서 의아하게 생각했던 것 같은데
Redux, React 등에서 사용하는 reducer를 보면 그런 의미 보다는 뭔가를 변경시키는 녀석한테 그런 이름을 붙이는 것 같다
useReducer는 usestate와 비슷하지만 state 하나가 가져야 할 값이 복잡하고 이를 수정하는 경우의 수가 많아진다면 성격이 비슷한 상태들을 묶어 관리할 수 있다
state에 대한 접근은 컴포넌트 내부에서 할 수 있고, 컴포넌트 바깥에서 상세한 정의를 해두고, 사전에 정의한 dispatcher로만 state의 업데이트를 가능하게 한다
(state를 사용하는 로직과, 이를 관리하는 로직을 분리할 수 있음)
예를 들어서 어떤 counter를 구현할 때, counter 숫자를 up, down, reset 하기 위한 업데이트 함수들을 useReducer를 이용하면 한 데서 관리할 수 있게 된다.
https://codesandbox.io/p/sandbox/counter-react-hook-usereducer-3fxtw?file=%2Fsrc%2Findex.js
forwardRef를 사용해 부모 컴포넌트에서 생성된 ref를 자식 컴포넌트가 props로 받아서 사용할 수 있는데,
자식 컴포넌트에서 이렇게 받은 ref를 원하는 대로 수정할 수 있게 해주는 것이 useImperativeHandle 훅이다
자식 컴포넌트에서 정의한 동작은 부모 컴포넌트에서 사용할 수 있다
const ChildInput = forwardRef((props,ref) => {
useImperativeHandle(ref,() => ({
alert:() => alert(props.value),
}),[props.value])
return <input ref={ref} {...props}/>
}
function Parent(){
const inputRef = useRef()
const [text,setText] = useState('')
function handleClick(){
//ChildInput 내부에서 useImperativeHandle로 추가한 동작을 사용할 수 있음
inputRef.current.alert()
}
function handleChange(e){
setText(e.target.value)
}
return (
<>
<ChildInput ref={inputRef} value={text} onChange={handleChange}/>
<button onClick={handleClick}>Focus</button>
</>
)
}
useEffect와 동일하게 동작하나 모든 DOM의 변경(렌더링)후에 콜백 함수 실행이 동기적으로 발생한다
useLayoutEffect가 useEffect보다 항상 먼저 실행된다
리액트 컴포넌트가 useLayoutEffect가 완료될 때까지 기다리기 때문에
DOM은 계산되었지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때와 같이 반드시 필요한 경우에만 사용해야 함
특정 요소에 따라 DOM 요소를 기반으로 한 애니메이션, 스크롤 위치 제어 등
화면에 반영하기 전에 하고 싶은 작업에 사용하면 useEffect보다 자연스러운 UX 제공 가능
디버깅하고 싶은 정보를 훅에 사용하면 리액트 개발자 도구에서 볼 수 있음
다른 훅 내부에서만 실행할 수 있고, 컴포넌트 레벨에서는 실행되지 않음
공통 훅을 제공하는 라이브러리나 대규모 웹 애플리케이션에서 디버깅 관련 정보를 제공하고 싶을 때 유용하게 사용 가능
리액트 프로젝트를 살펴보면서 문득 사용자 정의 훅은 그냥 함수와 뭐가 다른 거고, 어떨 때 사용하는 걸까,그리고 사용자 정의 훅이 아닌 다른 방안에는 어떤 것이 있을까 하는 궁금증이 생겼다
책에서 사용자 정의 훅과 고차 컴포넌트에서 소개하는 부분이 있는데, 그러한 궁금증이 조금이나마 풀리는 것 같아 정리해본다
사용자 정의 훅은 리액트에서 제공하는 훅인 useState, useEffect를 사용해서 공통 로직을 분리할 수 있을 때 사용할 수 있다
사용자 정의 훅 자체로는 렌더링에 영향을 미칠 수 없으므로 반환하는 값으로 무엇을 할 지는 개발자에게 달려있게 되는데,
컴포넌트 내부에 미치는 영향을 최소화해 개발자가 훅을 원하는 방향으로만 사용할 수 있다 (부수 효과가 비교적 제한적)
고차 컴포넌트는 어떤 일을 하는지, 어떤 결과물을 반환할 지는 고차 컴포넌트를 직접 보거나 실행하기 전까지는 알 수 없고 대부분 렌더링에 영향을 미치는 로직이 존재하기 때문에 사용자 정의 훅에 비해 예측이 어렵다
에러 바운더리와 같이 어떤 특정 에러가 발생했을 때 현재 컴포넌트 대신 에러가 발생했음을 알릴 수 있는 컴포넌트를 노출한다고 할 때 고차 컴포넌트를 사용할 수 있다
(사용자 정의 훅을 사용한다고 했을 때 에러 시 반환할 컴포넌트에 대한 로직이 반복적으로 들어가게 되므로, 고차 컴포넌트 사용보다 비효율적일 수 있다)
다만 고차 컴포넌트가 많아지면 복잡성이 증가하므로 신중하게 사용해야 한다
리액트 훅에 대한 정보 저장은 리액트 내부에서 특정 키를 기반으로 저장된다
useState, useEffect는 순서에 영향을 받는데 (next 키에 대한 값으로 다음 호출 순서들이 저장된다) 그렇기 때문에 순서를 보장받을 수 있도록 훅에 대한 규칙을 지키는 게 중요하다
안녕하세요! 글 잘 봤습니다!! 감사합니다!