// 내부 작동 자체만을 구현
const MyReact = (function(){
const global = {}
let index = 0
function useState(initialState){
if(!global.states){
// 애플리케이션 전체 states 배열 초기화. 최초 접근이면 빈 배열로 초기화
global.states = []
}
// states 정보를 조회해서 현재 상태값 있는지 확인, 없으면 초깃값으로 설정
const currentState = global.states[index] || initialState
global.states[index] = currentState
const setState = (function(){
// 현재 index를 클로저로 가둬놔서 이후에도
// 계속해서 동일한 index에 접근할 수 있도록 한다.
let currentIndex = index
return function(value){
global.states[currentIndex] = value
// 컴포넌트 렌더링(코드 생략)
}
})()
// useState를 쓸 때 마다 index를 하나씩 추가한다.
// index는 setState에서 사용된다.
// 즉 하나의 state마다 index가 할당되어 있어 그 index가 배열의 값(global.states)을 가리킨다.
index = index + 1
return [currentState, setState]
}
})
import {useState} from 'react'
export default function App(){
const [state, setState] = useState(() => {
// App 컴포넌트가 처음 구동될 때만 실행되고, 이후 리렌더링 시에는 실행되지 않는다.
console.log('복잡한 연산..')
return 0
})
function handleClick(){
setState((prev) => prev + 1)
}
return (
<div>
<h1>{state}</h1>
<button onClick={handleClick}>+</button>
</div>
)
}
의존성 없는 useEffect vs 그냥 실행
// 1
function Component(){
console.log('렌더링됨')
}
// 2
function Component(){
useEffect(() => {
console.log('렌더링됨)
})
}
const MyReact = (function(){
const global = {}
let index = 0
function useEffect(callback, dependencies){
const hooks = global.hooks
// 이전 훅 정보가 있는지 확인한다.
let previousDependencies = hooks[index]
// 변경됐는지 확인한다.
// 이전 값이 있다면 이전 값을 얕은 비교로 비교해 변경이 일어났는지 확인한다.
// 이전 값이 없다면 최초 실행이므로 변경이 일어난 것으로 간주해 실행을 유도한다.
let isDependenciesChanged = previousDependencies
? dependencies.some(
(value, idx) => !Object.is(value, previousDependencies[idx]).
)
: true
// 변경이 일어났다면 첫 번째 인수인 콜백 함수를 실행한다.
if(isDependenciesChanged){
callback()
// 다음 훅이 일어날 때를 대비하기 위해 index 추가
index ++
// 현재 의존성을 훅에 다시 저장한다.
hooks[index] = dependencies
}
}
return { useEffect }
})()
eslint-disable-line react-hooks/exhaustive-deps 주석 자제하기
첫 번째 인수에 함수명을 부여하기
거대한 useEffect 만들지 않기
불필요한 외부 함수를 만들지 마라
콜백 인수로 비동기 함수를 바로 넣을 수 없다
비동기 함수의 응답 속도에 따라 결과가 이상하게 나타날 수 있다.
// error
useEffect(() => {
const response = await fetch('...some url')
const result = await response.json()
setData(result)
},[])
// good
useEffect(() => {
let shouldIgnore = false
async function fetchData(){
const response = await fetch('...some url')
const result = await response.json()
if(!shouldIgnore){
setData(result)
}
}
fetchData()
return () => {
shouldIgnore = true
}
},[])
useEffect의 경쟁 상대(race condition)라 한다.
비동기 함수가 내부에 존재하게 되면 useEffect 내부에서 함수가 생성되고 실행되는 것을 반복 -> 클린업 함수에서 이전 비동기 함수 처리를 하는 것이 좋다.
state의 경쟁 상태 야기, cleanup 함수 실행순서도 보장할 수 없기 때문에 편의성을 위해 비동기 함수를 인수로 받지 않는다.
비용이 큰 연산에 대한 결과를 저장(메모이제이션)해 두고, 이 저장된 값을 반환하는 훅
import { useMemo } from 'react'
const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b])
const ChildComponent = memo(({ name, value, onChange }) => {
return (
<>
<h1>
{name} {value ? '켜짐' : '꺼짐'}
</h1>
<button 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의 자식 컴포넌트 전체가 렌더링된다.
// 한 버튼을 클릭해도 둘 다 렌더링된다.
// state 값이 바뀌면서 App이 리렌더링 -> onChange로 넘기는 함수가 재생성되고 있기 때문이다.
// 위 예제에서 useCallback 추가
const toggle1 = useCallback(() => {
setStatus1(!status1)
}, [status1])
const toggle2 = useCallback(() => {
setStatus2(!status2)
}, [status2])
// 의존성이 변경됐을 때만 함수가 재생성된다.
function RefComponent(){
const inputRef = useRef()
console.log(inputRef.current) // useRef의 최초 기본값은 DOM이 아니고 useRef()로 넘겨받은 인수이다. -> 선언된 당시에는 렌더링 전이라 return 전이기 때문에 undefined
useEffect(() => {
console.log(inputRef.current) // <input type="text"></input>
}, [inputRef])
return <input ref={inputRef} type="text"/>
}
function usePrevious(value){
const ref = useRef()
useEffect(() => {
ref.current = value
},[value]) // value가 변경되면 그 값을 value에 넣어준다
return ref.current
}
function SomeComponent(){
const [counter, setCounter] = useState(0)
const previousCounter = usePrevious(counter)
function handleClick(){
setCounter((prev) => prev + 1)
}
return (
<button onClick={handleClick}>
{counter} {previousCounter}
</button>
)
// 0 undefined
// 1, 0
// 2, 1
// 3, 2
}
useRef 구현
export function useRef(initialValue){
currentHook = 5
return useMemo(() => ({ current : initialValue }),[])
}
type State = {
count : number
}
type Action = { type: 'up' | 'down' | 'reset'; payload?: State }
// count를 받아 초깃값을 어떻게 정의할지 연산
function init(count: State) : State {
return count
}
const initialState : State = { count : 0 }
// state, action을 기반으로 state가 어떻게 변경될 지 정의
function reducer(state: State, action : Action) : State{
switch(action.type){
case 'up':
return { count: state.count + 1 }
case 'down':
return { count: state.count - 1 > 0 ? state.count - 1 : 0 }
case 'reset':
return init(action.payload || { count: 0 })
default:
throw new Error(`Unexpected action type ${action.type}`)
}
}
export default function App(){
const [state, dispatcher] = useReducer(reducer, initialState, init)
function handleUpButtonClick(){
dispatcher({ type: 'up' })
}
function handleDownButtonClick(){
dispatcher({ type: 'down' })
}
function handleResetButtonClick(){
dispatcher({ type: 'reset', payload: { count: 1}})
}
return(
<div className="App">
<h1>{state.count}</h1>
<button onClick={handleUpButtonClick}>+</button>
<button onClick={handleDownButtonClick}>-</button>
<button onClick={handleResetButtonClick}>reset</button>
</div>
)
}
const Input = forwareRef((props, ref) => {
// ref의 동작을 추가로 정의할 수 있다.
useImperativeHandle(
ref,
() => ({
alert: () => alert(props.value)
}),
[props.value]
)
return <input ref={ref} {...props}/>
})
function App(){
const inputRef = useRef()
const [text, setText] = useState('')
function handleChange(e){
setText(e.target.value)
}
return (
<>
<Input ref={inputRef} value={text} onChange={handleChange}/>
<button onClick={handleClick}>Focus</button>
</>
)
}
이 함수의 시그니처는 useEffect와 동일하나, 모든 DOM의 변경 후에 동기적으로 발생한다
두 훅의 형태나 사용 예제가 동일하다
DOM 변경 = 렌더링
실행 순서
useLayoutEffect의 실행이 종료될 때까지 기다린 다음에 화면을 그린다.
DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 등 반드시 필요할 떄만 사용해야 한다.
function useDate() {
const date = new Date();
useDebugValue(date, (date) => `현재 시간 : ${date.toISOString()}`);
return date;
}
export default function App() {
const date = useDate();
const [counter, setCounter] = useState(0);
function handleClick() {
setCounter((prev) => prev + 1);
}
return (
<div className="App">
<h1>
{counter} {date.toISOString()}
</h1>
<button onClick={handleClick}>+</button>
</div>
);
}
- 최상위에서만 훅을 호출해야 한다. 반복문, 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다. 이 규칙을 따라야만 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다.
- 훅을 호출할 수 있는 것은 리액트 함수 컴포넌트, 혹은 사용자 정의 훅 두 가지 경우 뿐이다. 일반 JS 함수에서는 훅을 사용할 수 없다.
function Component(){
const [count, setCount] = useState(0)
const [required, setRequired] = useState(false)
useEffect(() => {
// do something
},[count, required])
}
// 파이버에는 이렇게 저장된다.
{
memoizedState: 0, // setCount 훅
baseState: 0,
queue: { /* ... */ },
baseUpdate: null,
next: { // setRequired 훅
memoizedState: false,
baseState: false,
queue: { /* ... */ },
baseUpdate: null,
next: { // useEffect 훅
memoizedState: {
tag: 192,
create: () => {},
destroy: undefined,
daps: [0, false],
next: { /* ... */ },
},
baseState: null,
queue: null,
baseUpdate: null
}
}
}
리액트 훅은 파이버 객체의 링크드 리스트의 호출 순서에 따라 저장된다.
따라서 항상 훅은 실행 순서를 보장받을 수 있는 컴포넌트 최상단에 선언돼 있어야 한다.
사용자 인증 정보에 따라서 인증된 사용자 / 그렇지 않은 사용자에게 다른 컴포넌트를 보여주는 시나리오
interface LoginProps {
loginRequired?: boolean
}
function withLoginComponent<T>(Component: ComponentType<T>){
return function(props: T & LoginProps){
const {loginRequired, ...restProps} = props
if(loginRequired){
return <>로그인이 필요합니다.</>
}
return <Component {...(restProps as T)} />
}
}
// 원래 구현하고자 하는 컴포넌트를 만들고 withLoginComponent로 감싼다.
// 로그인 여부에 따라 다른 컴포넌트를 렌더링하는 책임을 모두 고차 컴포넌트에 맡긴다.
const Component = withLoginComponent((props : {value: string}) => {
return <h3>{props.value}</h3>
})
export default function App(){
const isLogin = true
return <Component value="text" loginRequired={isLogin}/>
}