You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
위와 같은 작업은 handleClick이 호출되어 리렌더링 될때마다 비싼 연산을 계속 한다.
이를 방지하기 위한 것이 Lazy Initial State
비싼 연산을 하는 함수를 호출한 결과를 초기값으로 넣는 것이 아니라, 비싼 연산을 하는 함수를 호출하는 함수를 useState의 파라미터에 콜백으로 넣는다.
// Counter 함수 컴포넌트의 함수 몸체 내부// 비싼 연산을 하는 함수를 매번 호출하는 초기값constexpensivelyCalculatedCount=getCount()const[count,setCount]=useState(expensivelyCalculatedCount)// 비싼 연산을 하는 함수를 콜백으로 넣은 초기값const[count,setCount]=useState(()=>getCount())
이렇게 함수를 넘기면 React는 함수가 반환한 값을 한 번만 계산하여 사용, 그 뒤에 일어나는 렌더링 때는 계산하지 않는다.
useState의 특성
setter함수 실행한다고 바로 렌더링에 반영되지 않는다.
새로운 값을 받으면 실행 시 곧바로 렌더링을 다시 하는 것이 아니라, component rerendering queue에 등록해두고 React가 나중에 처리한다.
새 값들을 모아서 리렌더링에 사용하는 batch 처리방식
function component에서 리렌더링 시 모든 함수가 재실행되지만 useState는 한 번만 실행된다.
setter함수는 동일성이 보장된다. 즉 첫 번째로 반환했던 setter함수의 값을 리액트가 기억하고 있으므로, 재실행하더라도 새 함수를 만들어 반환하지 않고 기존함수를 사용한다.
setter함수는 dependency array에 넣지 않아도 된다.
useState에 콜백함수를 전달하면 이 함수는 초기 렌더링에만 실행된다.
콜백함수가 반환하는 값을 초기 렌더링에 사용하고 그 뒤로는 값이 무시된다.
같은 값으로 setter함수를 호출하여 갱신하는 경우, 리렌더링은 무시된다.
React는 이전 상태와 새 상태가 같다면 함수 컴포넌트를 실행(리렌더링)하지 않는다.
setter 함수에 현재 state와 같은 값을 인자로 전달하면, React는 자식을 렌더링하거나 다른 함수를 실행하는 등의 행동을 하지 않는다.
state의 변경 감지는 Object.is 알고리즘을 사용하며, 이 알고리즘은 === 연산보다 엄격
결론: 리액트는 같은 값의 state로 setter함수를 실행하는 경우 ObjectIs 알고리즘을 사용하여 무시하는 방식으로 자체최적화
useMemo
useState로 비싼 연산을 한 번만 실행하기 위해 초기값으로 함수를 넣는 식으로 처리하는 방식을 배웠다.
콜백으로 넘긴 그 비싼 연산 함수를 초기값 설정 뿐 아니라 특정 상황 업데이트될 때 실행시켜야 한다면?
예컨대 장바구니에서 이것저것 넣을 때마다 주문서라는 component에서는 총 주문금액 등을 새로 계산하여야 한다. 이럴 때 useState보다는 useMemo로 실행하면 좋다.
useMemo는 Memoization 기법을 활용하며, parameter가 똑같다면 계산하지 않는다.
parameter 두개를 받는다
함수: 비싼 연산을 실행하고 그 결과값을 반환하는 함수
배열: dependency array, 이 안의 요소가 하나라도 변하면 첫 번째로 전달된 함수를 실행하여 반환한다. 이후 설명할 useEffect 등에서도 동일
functiongetCount(fillNumber){console.log('An expensive work is been executing...')returnArray(100000).fill(fillNumber).reduce((acc,cur)=>acc+cur,0)}functionApp(){return(<Counter/>)}functionCounter(){const[fillNumber,setFillNumber]=useState(1)const[text,setText]=useState('')constsum=getCount(fillNumber)consthandleClick=()=>{setFillNumber((previousNumber)=>previousNumber+1)}return(<section><span>Sum: {sum}</span><inputvalue={text}onChange={(event)=>setText(event.target.value)}/><buttononClick={handleClick}>
Plus FillNumber 1
</button></section>)}
위 코드에서, input 태그에 Change event가 발생할 경우마다 sum이라는 변수에 할당된 getCount 함수가 연산식을 실행한다.
우리가 의도한 것은 fillNumber가 바뀔 때만 sum을 계산하는 것이므로, sum에 할당하는 getCount를 useMemo로 넣어준다.
// Counter 함수 콤포넌트의 함수 몸체 내부// 기존 비싼 연산을 바로 할당하는 경우constsum=getCount(fillNumber)// useMemo를 사용하여 비싼 연산을 필요할 때만 실행하게 하는 경우constsum=useMemo(()=>getCount(fillNumber),[fillNumber])
비싼 연산을 하는 getCount 함수가 우리가 의도한대로 fillNumber값이 바뀔 때에만 수행된다.
dependency array에 넘긴 값에 변경이 있을 때 useMemo에 첫 번째로 넘긴 함수가 실행
dep array에 넘긴 값이 동일하다면 그 값에 대해 연산한 결과를 메모로 기억하고 있다가 해당 연산결과를 반환한다.
useMemo의 특성
최적화를 위해 사용될 수 있지만 항상 최적화를 보장하지는 않는다.
Memoization을 한다는 것은 곧 memory에 값을 저장한다는 것이므로 불필요하게 많은 값을 메모하면 오히려 성능이 떨어진다.
정말 이 값을 메모해야하는지 고민한 후 사용할 것
단순한 값 자체를 메모이제이션 할 때 사용
useMemo로 전달된 함수는 렌더링 중에 실행된다.
렌더링 후 호출하는 함수는 useEffect에서 해야 한다.
Component Memoization
컴포넌트 내부에서 상태를 memo할 수 있다면 컴포넌트 자체도 memo가 가능할까? React.memo로 가능!
function component는 props라는 하나의 파라미터를 받아서 UI 결과를 반환하는 함수일 뿐이다.
즉, 컴포넌트에 전달되는 props만 이전과 같다면 반환값도 동일하므로 memo를 통해 리렌더링을 방지, CPU연산을 아낄 수 있다.
import를 해준 후 React.memo(function MyComponent(props) { 함수 콤포넌트 몸체 })의 형태로 메모가 가능하다.
내부적으로 useState, useReducer. useContext 등의 상태변화 훅을 가지고 있어서 렌더링을 유발하는 경우라면 컴포넌트 메모의 의미가 없어진다.
위의 예제에서 input이 change event를 발생시킬 때마다 rendering 되는 경우 등
기본적으로 memo함수는 함수컴포넌트가 받는 props의 기존 객체와 새 객체를 shallow compare을 통해 비교, 객체의 레퍼런스만 비교하여 업데이트 여부를 판단한다.
리터럴 객체를 넘기는 경우 reference 주소가 매번 달라지므로 리렌더링이 된다. 이를 방지하거나 deep compare로 컴포넌트 렌더링을 통제하고자 한다면 memo의 두 번째 파라미터로 비교 함수를 만들어 전달하면 된다.
이전 props와 현재 props를 비교하는 비교함수로 areEqual을 만들어 내용물을 비교해주는 함수를 넣어주거나 또는 Json.stringify()를 사용하여 비교하면 된다.
해당 콤포넌트를 export할 때 export default memo(Counter, areEqual)로 메모함수로 감싸 export한다.
functionMyComponent(props){/* props를 사용한 렌더링 로직이 있는 함수콤포넌트 몸체 */}functionareEqual(prevProps,nextProps){// prevProps와 nextProps 비교 로직 (예시)functionisSameObject(obj1,obj2){returnObject.entries(obj1).every(([key,value]=>{if(obj2[key]===value)returntrue;}));}returnisSameObject(prevProps,nextProps);}exportdefaultmemo(MyComponent,areEqual);
useEffect
Function Component의 Life cycle method를 담당하는 훅 중 하나
class component에서는 componentDidMount, componentDidUpdate, componentWillUnMount 등의 메서드를 사용했다.
이중 componentDidMount, componentDidUpdate, componentWillUnmount의 역할을 한다.
렌더링이 모두 완료된 후 호출
함수와 배열을 파라미터로 받는다.
리렌더링 될때마다 계속 실행되는데, 이를 방지하는 dependency array가 두 번째 파라미터로 오는 배열이다.
첫번째로 넘기는 함수가 다른 함수를 반환하면, 반환된 함수는 unmount시점에 실행
mount / unmount 시점에 주로 일어나는 일들
mount: API 요청, web socket 연결, 3rd party lib 활용, timeout이나 interval set
unmount: web socket 자원 정리, lib 자원 정리, timeout이나 interval clear
useEffect의 특성
렌더링 완료 이후 비동기적으로 실행된다.
useEffect의 콜백함수는 리액트가 전달된 콜백함수를 기억하고 있다가 실행하기 때문에 렌더링 이후 실행이 보장된다. 또한 그 다음 렌더링 이전의 실행을 보장한다.
렌더링 결과가 화면에 모두 그려진 뒤 비동기적으로 실행하므로 일부 상태를 즉시 변경할 필요가 없는 경우나, 이벤트 핸들러를 관리하는 경우 사용
동기적으로 실행하여 레이아웃 측정 등의 작업을 해야 하는 경우에는 useLayoutEffect를 사용
매 렌더링 이후 실행되지만, dep array에 주어진 값이 변경되지 않았다면 실행하지 않는다.
매번 clean up도 실행: 업데이트 된 새로운 값으로 실행되면서, 업데이트 된 props 값을 반영하여 버그를 줄인다.
콜백으로 익명함수를 넘기는 이유는 클로저로 컴포넌트 내의 참조값을 제대로 업데이트 하기 위함이다.
useLayoutEffect
모든 DOM 변경 이후 동기적으로 실행하며, 모든 DOM이 업데이트되고 나서 DOM을 변경하려는 경우나, 상태 업데이트로 UI가 깜박거리는 경우를 방지하기 위해 사용
useEffect와 함께 Function Component의 Life cycle method를 담당하는 훅 중 하나
useEffect는 렌더링 완료 이후 실행되는데, useEffect에 전달되는 함수에서 DOM
에 직접 접근하여 변경해야 하는 함수를 넣으면, UI가 변경되고 난 후 렌더링을 또 하면서 깜박거림이 생긴다.
그러므로 LifeCycle 내에서 직접 UI를 변경해야 하는 경우에는 useEffect가 아니라 useLayoutEffect로 한다.
useCallback
useMemo로 함수를 메모한 것과 동일하다. 반환값으로 값을 넘기냐 함수를 넘기냐의 차이일 뿐
useCallback(func, dep array) == useMemo(() => func, dep array)
첫 번째 인자로 memo할 함수를 넘기며, 두 번째 인자로 언제 함수가 재할당되어야하는지 결정하는 dependency array를 넘기면 dep array가 바뀔 때 메모된 함수가 콜백된다.
함수가 만들어지고 할당되는 과정에서 반복되는 계산을 방지한다.
useRef
component의 생명주기나 rendering과 관계 없이 어떤 값을 꾸준히 유지시키거나 DOM에 접근할 때 사용
이처럼 JSX가 실제 DOM에 반영되고 나서 input태그의 참조를 inputEl이라는 key에 넣어주면 current로 들어가게 된다.
React에서는 document.~으로 가져오는 건 최대한 지양, useRef를 사용할 것
렌더링 후 바로 focus 주고 싶은 경우는 렌더링 이후 실행되는 useEffect를 사용하자.
useEffect(()=>{inputEl.current.focus();}
useRef 사용 시 주의사항
useRef 속의 current는 아무리 여러번 렌더링 되어도 값을 유지하며, current값을 수정한다고 해도 리렌더링이 되지 않는다.
React가 DOM노드에 ref를 붙이거나 뗄 때마다 어떤 동작을 수행하기 위해 리렌더링을 해야하면 useCallback으로 Callback Ref를 사용해야.
useCallback으로 만들어진 함수를 넣으면 React가 알아서 할당한다. useCallback에 넘겨지는 콜백함수의 첫 번째 인자는 DOM의 참조객체이다.
React가 렌더링이 끝나고 DOM의 ref를 current에 저장하고, useEffect를 실행한다.
값을 넣는 시점에 무언가 실행시키고 싶은데 useEffect를 쓰고 싶지 않은 경우 useCallback을 쓴다고 생각하자.
functionMeasureExample(){const[height,setHeight]=useState(0);constmeasuredRef=useCallback(node=>{if(node!==null){setHeight(node.getBoundingClientRect().height);}},[]);return(<><h1ref={measuredRef}> Hello, world</h1><h2>The above header is {Math.round(height)}px tall.</h2></>);}
렌더링 후 h1 태그의 참조를 받아 해당 태그의 height값을 setHeight로 할당한다.
만약 이것을 useRef로 만들었는데 리렌더링 이후 CSS가 적용되어 height값이 바뀌면 setHeight로 설정해둔 height값은 이전 높이의 값을 여전히 가지고 있다.
forwardRef
여러군데에서 활용되고 있는 컴포넌트에서, 자식 컴포넌트에 대한 참조를 얻어야 하는 경우?
자신의 DOM이 아니라, 부모 component가 자식 component의 DOM객체 참조를 얻어서 무언가 작업해야 하는 경우 등
이런 경우를 위해 React는 함수 컴포넌트를 인자로 받는 forwardRef라는 함수를 제공한다.
함수 컴포넌트는 참조해야하는 component를 첫 번째 인자로, ref를 두 번째 인자로 받는다.
위 ref를 JSX의 ref 어트리뷰트에 전달하면서 함수 컴포넌트를 호출하면 해당하는 참조를 갖는 React 노드, 즉 함수 component가 반환된다.
constFancyButton=React.forwardRef(({ children },ref)=>(<buttonref={ref}className="FancyButton">{children}</button>));constref=React.createRef();<FancyButtonref={ref}>Click me!</FancyButton>
createRef를 쓰지 않고 컴포넌트에서 다른 컴포넌트를 렌더링하는 경우가 더 많다.
자식 컴포넌트에서 forwardRef로 보내면 부모 컴포넌트에서는 useRef로 받아서 사용
처음 실행할 때 count는 0으로 초기화되고, useRef가 만들어져 prevCountRef에 할당되고, 렌더링 이후 useEffect가 실행된다.
prevCount에는 현재 가진 값이 나타나고, 재실행되더라도 useRef는 실행되지 않으므로 count가 업데이트 되면 전에 있던 값을 물고 있다.
useImperativeHandle
forwardRef랑 같이 사용, 자식 component에서 특정 method를 부모component에게 넘겨야 하는 상황에서 사용
React에서 부모에서 자식으로 props 전달하는 게 일반적이지만, 다음과 같은 경우에 사용할 수 있다.
부모 컴포넌트가 자식 컴포넌트의 참조를 전적으로 갖는 것이 부담될 때
자식 컴포넌트에서 메서드를 부모 컴포넌트에게 넘겨야 할 때
첫 인자는 부모로부터 전달받은 ref를 받고 두번째 인자로 함수를 전달, 그 함수는 메서드를 담은 객체를 return 한다.
함수가 리턴하는 객체는 { key: value } 형태로 부모에게 알리고 싶은 동작을 custom하여 전달하며, 부모는 getValue로 이를 참조할 수 있다.
// ref와 메서드를 보내는 자식 componentconstInput=React.forwardRef((props,ref)=>{constinputRef=useRef(null)useImperativeHandle(ref,()=>({getValue:()=>{inputRef.current.focus()}}))return(<inputtype="text"ref={inputRef}/>)})// ref와 메서드를 받는 부모 componentfunctionApp(){constinputRef=React.useRef()return(<divclassName="App"><Inputref={inputRef}/><buttononclick={()=>{console.log(inputRef.current.getValue())}}>
focus
</button></div>)}
useReducer
한 컴포넌트에서 불러온 애를 다른 공간에서도 써야 할 때, 전역 상태 관리 도구인 redux를 주로 사용한다.
useReducer와 context API를 사용하면 비슷한 효과를 낼 수 있다.
예컨대 회원가입 리프레시하더라도 사용자가 입력한 값을 기억하여 UX를 높이고자 하는 상황을 생각해보자
local storage에 하나하나 저장해두는 방법도 있지만 구조가 잡혀있는 형태로 관리를 해야 나중에 편해진다.
전역상태로 들고 있는 정보를 통해 다른 component에서도 접근하여 활용하도록 해보자.
useReducer 활용 예제
정의해두고 싶은 복잡한 상태를 정의하고, 처음에는 빈값으로 채워두고 reducer이라는 함수를 작성하여 첫 번째 인자로 넘긴다.
두 번째 인자로 초기 상태를, 세 번째 인자로 초기화를 비동기적으로 실행하는 함수를 넘긴다.