3장 리액트 훅 깊게 살펴보기

함수형 컴포넌트가 상태를 사용하거나 클래스형 컴포넌트의 생명주기 메서드를 대체하는 등의 다양한 작업을 하기 위해 훅(hook)이라는 것이 추가됐다. 훅을 활용하면 클래스형 컴포넌트가 아니더라도 리액트의 다 양한 기능을 활용할 수 있다. 리액트에서 현재 사용 가능한 훅이 무엇이고, 어떻게 쓰이는지, 그리고 훅을 사용할 때 주의할 점은 무엇인지 확인해 보자.

3.1 리액트의 모든 훅 파헤치기

3.1.1 useState

function Component() {
    const [, triggerRender] = useState()
    let state = 'hello'
    function handleButtonClickO {
        state = 'hi'
        triggerRender()
    }
    return (
        <>
        <hl>{state}</hl>
        <button onClick={handleButtonClick}>hi</button>
        </>
    )
}
function useState(initialValue) {
    let internalstate = initialVaTue
    function setState(newValue) {
        internalstate = newValue
    }
    return [internalstate, setState]
}
const [value, setValue] = useState(0)
setValue(l)
console.log(value) // 0
function useState(initialValue) {
    let internalstate = initialvalue
    function state() {
        return internalstate
    }
    function setState(newValue) {
        internalstate = newValue
    }
    return [state, setState]
}

const [value, setValue] = useState(0)
setValue(l)
console.log(value()) // 1
게으른 초기화
// 일반적인 useState 사용
// 바로 값을 집어넣는다.
const [count, setCount] = useState(
    Number.parselnt(window.localstorage.getltem(cacheKey)),
)
// 게으른 초기화
// 위 코드와의 차이점은 함수를 실행해 값을 반환한다는 것이다.
const [count, setCount] = useState(() =>
    Number.parselnt(window.localstorage.getltem(cacheKey)),
)

3.1.2 useEffect

클린업 함수의 목적
useEffect(() => {
function addMouseEvent() {
    console.log(counter)
}
window.addEventListener('click', addMouseEvent)
// 클린업 함수
return () => {
    console.log('클린업 함수 실행!', counter)
    window. removeEventListerier('click', addMouseEvent)
}
}, [counter])
클린업 함수 실행! 0
1

클린업 함수 실행! 1
2

클린업 함수 실행! 2
3

클린업 함수 실행! 3
4

// ...
의존성 배열
// 1
function Component() {
    console.log('렌더링됨')
}
// 2
function Component() {
    useEffect(() => {
        console.log('렌더링됨')
    })
}
useEffect를 사용할 때 주의할 점
function Component({ log }: { log: string }) {
    useEffect(() => {
        logging(log)
    }, []) // eslint-disable-line react-hooks/exhaustive-deps
}
useEffect의 첫 번째 인수에 함수명을 부여하라
useEffect(() => {
    logging(user.id)
}, [user.id])
useEffect(
    function logActiveUser() {
        logging(user.id)
    },
    [user.id],
)
거대한 useEffect를 만들지 마라
불필요한 외부 함수를 만들지 마라
useEffect(() => {
    fetchlnfonnation(id)
    return () => controUerRef.current?.abort()
}, [id, fetchinformation])
useEffect(() => {
    const controller = new AbortController()
    ;(async () => {
        const result = await fetchlnfo(id, { signal: controller.signal })
        setlnfo(await result.json())
    })()
    return () => controller.abort()
}, [id])

3.1.3 useMemo

import { useMemo } from 'react'
const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b])

3.1.4 useCallback

const Childcomponent = memo(({ name, value, onChange }) => {
    // 렌더링이 수행되는지 확인하기 위해 넣었다.
    useEffect(() => {
        console.log('rendering!', name)
    })
    return (
        <>
            <hl>
                {name} {value ? '켜짐' : '꺼짐'}
            </hl>
            <button onClick={onChange}>toggle</button>
        </>
    )
})

function App() {
    const [status1, setStatus1] = useState(false)
    const [status2, setStatus2] = useState(false)

    const togglel =()=>{
        setStatusl(!status1)
    }
    const toggle2 =()=>{
        setStatus2(!status2)
    }
    return (
        <>
        <ChildComponent name="l" value={status1} onChange={togglel} />
        <ChildConiponent name="2" value={status2} onChange={toggle2} />
        </>
    )
}
const toggle1 = useCallback(
    function toggle1() {
        setStatusl(!status1)
    },
    [status1],
)
const toggle2 = useCallback(
    function toggle2() {
        setStatus2(!status2)
    },
    [status2],
)

3.1.5 useRef

let value = 0
function Component() {
    function handleClick() {
        value += 1
    }

    // ...
}

3.1.6 useContext

Context란?
Context를 함수형 컴포넌트에서 사용할 수 있게 해주는 useContext 혹
const Context = createContext<{ hello: string } | undefined>()
function ParentComponent() {
    return (
        <>
            <Context.Provider value=>
                <Context.Provider value=>
                    <ChiIdComponent />
                </Context.Provider            </Context.Provider        </>
    )
}

function ChildComponent() {
    const value = useContext(Context)
    // react가 아닌 javascript가 반환된다.
    return <>{value ? value.hello : ''}</>
}
function useMyContext() {
    const context = useContext(MyContext)
    if (context === undefined) {
        throw new Error(
        'useMyContext는 Contextprovider 내부에서만 사용할 수 있습니다.',
        )
    }
    return context
}
useContext를 사용할 때 주의할 점
const MyContext = createContext<{ hello: string } | undefined>(undefined)
function ContextProvider({
    children,
    text,
}: PropsWithChildren<{ text: string }>) {
    return (
        <MyContext.Provider value=>{children}</MyContext.Provider>
    )
}
function useMyContext() {
    const context = useContext(MyContext)
    if (context === undefined) {
        throw new Error(
        'useMyContext는 Contextprovider 내부에서만 사용할 수 있습니다.',
        )
    }
    return context
}

function GrandChildComponent() {
    const { hello } = useMyContext()
    useEffect(() => {
        console.log('렌더링 GrandChildComponent')
    })
    return <h3>{hello}</h3>
}

function ChildComponent() {
    useEffect(() => {
        console.log('렌더링 Childcomponent')
    })
    return <GrandChildConiponent />
}

function ParentComponent() {
    const [text, setText] = useStateC('')
    function handleChange(e: ChangeEvent<HTMLInpiitElement>) {
        setText(e.target.value)
    }
    useEffect(() => {
        console.log('렌더링 Parentcomponent‘)
    })

    return (
        <>
            <ContextProvider text="react">
                <input value={text} onChange={handleChange} />
                <ChildComponent />
            </ContextProvider>
        </>
    )
}

3.1.7 useReducer

// useReducer가 사용할 state를 정의
type State = {
    count: number
}

// state의 변화를 발생시킬 action의 타입과 넘겨줄 값(payload)을 정의
// 꼭 type과 payload라는 네이밍을 지킬 필요도 없으며, 굳이 객체일 필요도 없다.
// 다만 이러한 네이밍이 가장 널리 쓰인다.
type Action = { type 'up' | 'down' | 'reset'; payload?: State }

// 무거운 연산이 포함된 게으른 초기화 함수
function init(count State) State {
    // count: 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>
    )
}

3.1.8 useImperativeHandle

forwardRef 살펴보기
function ChildComponent({ ref }) {
    useEffect(() => {
        // undefined
        console.log(ref)
    }, [ref])
    return <div>안녕!</div>
}

function ParentComponent() {
    const inputRef = useRef()
    return (
        <>
        <input ref={inputRef} />
        {/* 'ref' is not a prop. Trying to access it will result in 'undefined' being returned. If you
        need to access the same value within the child component, you should pass it as a different prop */}
        ChildComponent ref={inputRef} />
        </>
    )
}
function ChildComponent({ parentRef }) {
    useEffect(() => {
        // {current; undefined}
        // {current: HTMLInputElement}
        console.log(parentRef)
    }, [parentRef])
    return <div>안녕! </div>
}

function ParentComponent() {
    const inputRef = useRef()
        return (
            <>
            <input ref={inputRef} />
            <ChildComponent parentRef={inputRef} />
            </>
        )
}
const Childcomponent = forwardRef((props, ref) => {
    useEffect(() => {
        // {current: undefined}
        // {current: HTMLInputElement}
        console.log(ref)
    }, [ref])
return <div>안녕!</div>
})

function ParentComponent() {
    const inputRef = useRef()
    return (
        <>
        <input ref={inputRef} />
        <ChildComponent ref={inputRef} />
        </>
    )
}
useImperativeHandle이란?
const Input = forwardRef((props, ref) => {
    // useImperativeHandle을 사용하면 ref의 동작을 추가로 정의할 수 있다.
    useImperativeHandle(
        ref,
        () => ({
            alert () => alert(props.value),
        }),
        // useEffect의 deps와 같다.
        [props.value].
    )
    return <input ref={ref} {...props} />
})

3.1.9 useLayoutEffect

3.1.11 훅의 규칙

3.2 사용자 정의 혹과 고차 컴포넌트 중 무엇을 써야 할까?

3.2.1 사용자 정의 훅

import { useEffect, useState } from 'react'
// HTTP 요청을 하는 사용자 정의 훅
function useFetch<T>(
url: string,
{ method, body }: { method: string; body?: XHLHttpRequestBodylnit },
( {
    // 응답 결과
    const [result, setResult] = useState<T | undefined>()
    // 요청 중 여부
    const [isLoading, setlsLoading] = useState<boolean>(false)
    // 2xx 3xx로 정상 응답인지 여부
    const [ok, setOk] = useState<boolean | undefined>()
    // HTTP status
    const [status, setStatus] = useState<number | undefined>()
    useEffect(() => {
        const abortControUer = new AbortControUer()
        ;(async () => {
            setlsLoading(true)
            const response = await fetch(url, {
                methodj
                body,
                signal: abortcontroller.signal,
            })
            setOk(response.ok)
            setStatus(response.status)
            if (response.ok) {
                const apiResult = await response.json()
                setResult(apiRes냐It)
            }
            setlsLoading(false)
        })()
        return () => {
        abortcontroller.abort()
        }
    }, [url, method, body])
    return { ok, result, isLoading, status }
}

3.2.2 고차 컴포넌트

React.memo란?
const Childcomponent = ({ value }: { value: string }) => {
    useEffect(() => {
        console.log(' 렌더링 !')
    })
    return <>안녕하세요! {value}</>
}

function ParentComponent() {
    const [state, setState] = useState(l)
    function handleChange(e: ChangeEvent<HTMLInputEleinent>) {
        setState(Number(e.target.value))
    }
    return (
    <>
        <input type="number" value={state} onChange={handleChange} />
        <ChildComponent value="hello" />
    </>
    )
}
const Childcomponent = memo(({ value }: { value string }) => {
    useEffect(() => {
        console.log('렌더링!')
    })
    return <>안녕하세요! {value}</>
})
고차 함수 만들어보기
const list = [1, 2, 3]
const doubledList = list.map((item) => item * 2)
// 즉시 실행 함수로 setter를 만든다.
const setstate = (function () {
    // 현재 index를 클로저로 가둬놔서 이후에도 계속해서 동일한 index에
    // 접근할 수 있도록 한다.
    let currentindex = index
    return function (value) {
        global.states[currentlndex] = value
        // 컴포넌트를 렌더링한다. 실제로 컴포넌트를 렌더링하는 코드는 생략했다.
    }
})()
function add(a) {
    return function (b) {
        return a + b
    }
}

const result = add(1) // 여기서 result는 앞서 반환한 함수를 가리킨다.
const result2 = result(2) // 비로소 a와 b룔 더한 3이 반환된다.
고차 함수를 활용한 리액트 고차 컴포넌트 만들어보기
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로 감싸기만 하면 끝이다.
// 로그인 여부, 로그인이 안 되면 다른 컴포넌트를 렌더링하는 책임은 모두
// 고차 컴포넌트인 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} />
    // return <Component value="text" />;
}
const Component = withHigherOrderComponentl(
    withHigher0rderComponent2(
        withHigher0rderComponent3(
            withHigher0rderComponent4(
                withHigher0rderCoinponent5(() => {
                    return <>안녕하세요.</>
                })>
            ),
        ),
    ),
)

3.2.3 사용자 정의훅과 고차 컴포넌트 중 무엇을 써야 할까?

사용자 정의 훅이 필요한 경우
// 사용자 정의 훅올 사용하는 경우
function HookComponent() {
    const { loggedln } = useLogin()

    useEffect(() => {
        if (!loggedln) {
        // do something..
        }
    }, [loggedln])
}
// 고차 컴포넌트를 사용하는 경우
const HOCComponent = withLoginComponent(() => {
// do something...
}
고차 컴포넌트를 사용해야 하는 경우
function HookComponent() {
    const { loggedln } = useLogin()
    if (!loggedln) {
        return <LoginComponent />
    }
    return <>안녕하세요.</>
}

const HOCComponent = withLoginComponent(() => {
    // loggedln state의 값을 신경 쓰지 않고 그냥 컴포넌트에 필요한 로직만
    // 추가해서 간단해졌다. loggedln state에 따른 제어는 고차 컴포넌트에서 해줄 것이다.
    return <>안녕하세요.</>
})

3.2.4 정리

끝!