⚡리액트 핵심요소 깊게 살펴보기 - 3.1 리액트의 모든 훅 파헤치기
🌚 훅(hook)
함수형 컴포넌트가 상태를 사용하거나 클래스형 컴포넌트의 생명주기 메서드를 대체하는 등의 다양한 작업을 하기 위해 추가됨
훅을 사용하면 클래스형 컴포넌트가 아니더라도 리액트의 다양한 기능을 활용할 수 있음
3.1 리액트의 모든 훅 파헤치기
3.1.1 useState
- 함수형 컴포넌트 내부에서 상태를 정의하고 상태를 관리할 수 있게 해주는 훅
🌚기본 사용법
import {useState} from 'react'
const {state, setState} = useState(initialState)
- useState 인수로는 state의 초기값을 넘겨주는데, 아무런 값을 넘겨주지 않으면 undefined가 초기값임
- useState 반환값 : 배열
- state:값 자체를 사용할 수 있음
- setState: 해당 state의 값을 변경함
💡useState를 사용하지 않고, 함수 내부에서 자체적으로 변수를 사용하여 상태값을 관리하면?
- 함수형 컴포넌트는 매번 함수를 실행해 렌더링이 일어나고, 함수 내부의 값은 함수가 실행될때마다 다시 초기화됨
💡 useState 훅의 결과값은 어떻게 함수가 실행되어 그값을 유지할까?
- 리액트는 상태값을 메모리에 저장하고, setState는 그 저장공간을 가리키는 클로저를 반환함
그래서 컴포넌트 함수가 다시 실행돼도 값이 “유지되는 것처럼” 보임
🌕 게으른 초기화 (Lazy initialization)
- useState에 변수 대신 함수를 넘기는 것
//일반적인 useState 사용
//바로 값을 집어 넣음
const [count, setCount] = useState(
Number.parseInt(window.localStorage.getItem(cacheKey)),
)
//게으른 초기화
//함수를 실행해 값을 반환함
const [count, setCount] = useState(() =>
Number.parseInt(window.localStorage.getItem(cacheKey)),
)
- 게으른 초기화는 state가 처음 만들어질때만 사용함
- 함수형 컴포넌트는 렌더링될 때마다 함수 전체가 다시 실행됨
- 하지만 useState는 기존 state 값을 유지하고, 초기값은 첫 렌더링 때만 사용됨
💡게으른 초기화 언제 함수형 초기화를 쓰면 좋은가
: useState(값) 대신 useState(() => 값) 형태로 작성하는 게 성능적으로 이득
- 무거운 연산이 초기값에 포함될 때
- localStorage, sessionStorage 접근
- 배열 메서드 (map, filter, find)로 복잡한 계산
3.1.2 useEffect
- 애플리케이션 내 컴포넌트의 여러 값을 활용해 동기적으로 부수 효과를 만드는 메커니즘
- 부수 효과가 언제 일어나는지보다 언제 상태값과 함께 실행되는지 살펴보는 것이 중요함
🌚기본 사용법
function Component() {
useEffect(() => {
//
},[props,state])
}
첫번째 인수 : 실행할 부수 효과가 포함된 함수
두번째 인수 : 의존성 배열(빈배열or배열생략 가능)
💡useEffect는 어떻게 의존성 배열이 변경된 것을 알고 실행될까?
- 함수형 컴포넌트는 매번 함수를 실행해 렌더링을 수행한다는 것
- 렌더링할 때마다 의존성에 있는 값을 보면서 의존성의 값이 이전과 다른 게 하나라도 있으면 부수 효과를 실행하는 것
🌕 클린업 함수의 목적
import { useState, useEffect } from 'react'
export default function App() {
const [counter, setCounter] = useState(0)
function handleClick() {
setCounter((prev) => prev + 1)
}
useEffect(() => {
function addMouseEvent() {
console.log(counter)
}
window.addEventListener('click', addMouseEvent)
// 클린업 함수
return () => {
console.log('클린업 함수 실행!', counter)
window.removeEventListener('click', addMouseEvent)
}
}, [counter])
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+</button>
</>
)
}
//실행결과
클린업 함수 실행! 0
1
클린업 함수 실행! 1
2
클린업 함수 실행! 2
3
클린업 함수 실행! 2
4
- 클린업 함수는 새로운 값을 기반으로 렌더링 뒤에 실행되지만, 변경된 값을 읽는 것은 아니라 함수가 정의됐을 당시에 선언됐던 이전 값을 보고 실행된다는 것
- 함수형 컴포넌트의 useEffect는 콜백이 실행될 때마다 이전의 클린업 함수가 존재한다면, 클린업 함수를 실행한 뒤에 콜백을 실행함
- 따라서 이벤트를 추가하기 전에 이전에 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가하는 것
- 이렇게 함으로써 특정 이벤트의 핸들러가 무한히 추가되는 것을 방지할 수 있음
언마운트: 특정 컴포넌트가 DOM에서 사라진다는 것을 의미하는 클래스형 컴포넌트의 용어
클린업 함수는 언마운트라기보다는 함수형 컴포넌트가 리렌더링됐을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는, 말 그대로 이전 상태를 청소해 주는 개념임
🌕 의존성 배열
- 빈 배열을 두거나 값을 넘기지 않을때, 리액트가 useEffect는 비교할 의존성이 없다고 판단해 최초 렌더링 직후 실행된 다음부터는 더 이상 실행되지 않음
- 아무런 값도 넘겨주지 않는다면 이때는 의존성을 비교할 필요 없이 렌더링할 때마다 실행이 필요하다고 판단해 렌더링이 발생할 때마다 실행됨
빈 배열: 최초 렌더링때 실행된 이후 실행되지 않음
아무런 값 없을때 : 렌더링 할 때 마다 실행
원하는 값 : 값이 바뀔 때 마다 실행
💡의존성 배열에 아무런 값이 없는 useEffect가 매 렌더링마다 실행된다면useEffect 없이 써도 되는 게 아닐까?
// 1
function Component() {
console.log('렌더링됨');
}
// 2
function Component() {
useEffect(() => {
console.log('렌더링됨;);
});
}
💡위의 두 코드 차이점
- 서버 사이드 렌더링 관점에서 useEffect는 클라이언트 사이드에서 실행되는 것을 보장해 준다. useEffect 내부에서는 window 객체의 접근에 의존하는 코드를 사용해도 된다.
- useEffect는 컴포넌트 렌더링의 부수 효과, 즉 컴포넌트의 렌더링이 완료된 이후에 실행된다. 반면 직접 실행은 컴포넌트가 렌더링되는 도중에 실행된다. 따라서 위와는 달리 서버 사이드 렌더링의 경우에 서버에서도 실행된다. 그리고 이 작업은 함수형 컴포넌트의 반환을 지연시키는 행위다. 즉, 무거운 작업일 경우 렌더링을 방해하므로 성능에 악영향을 미칠 수 있다.
useEffect의 effect는 컴포넌트의 사이드 이펙트, 즉 부수 효과를 의미한다는 것을 명심하고, useEffect는 컴포넌트가 렌더링된 후에 어떠한 부수 효과를 일으키고 싶을 때 사용하는 훅임
🌚useEffect의 구현
- 의존성 배열의 이전 값고 현재 값의 얕은 비교임
- Object.is를 기반으로 하는 얕은 비교를 수행함
- 이전 의존성 배열과 현재 의존성 배열의 값에 하나라도 변경 사항이 있다면 callback으로 선언한 부수 효과를 실행하며, 이것이 useEffect의 본질임
🌚useEffect 사용시 주의해야 할 점
- eslint-disable-line react-hooks/exhaustive-deps 주석은 최대한 자제하라
- useEffect는 의존성 배열에 명시된 값의 변화에 의해 실행되는 훅이고, 의존성 배열을 생략하고 내부에서 값을 사용하면, 그 값의 변경과 effect 실행이 연결되지 않아 의도치 않은 동작이 발생함
- useEffect의 첫 번째 인수에 함수명을 부여하라
- useEffect가 단순할 땐 익명 함수도 괜찮지만, 복잡해질수록 함수명을 부여해 목적을 명확히 하고 책임을 좁히는 것이 좋음
- 거대한 useEffect를 만들지 마라
- useEffect는 렌더링 이후 실행되지만,복잡하거나 무거운 로직은 여전히 성능에 영향을 주기때문에, 큰 effect 하나보다 의존성이 적은 작은 effect 여러 개로 분리하는 것이 좋음
- 불필요한 외부 함수를 만들지 마라
- useEffect 내부에서 비동기 코드를 직접 사용하면 요청 순서가 뒤섞이는 경쟁 상태(race condition) 가 발생할 수 있음
- React는 이를 방지하고 cleanup 순서를 보장하기 위해 useEffect(async () => …) 형태를 지원하지 않음
3.1.3 useMemo
- 비용이 큰 연산에 대한 결과를 저장해두고, 저장된 값을 반환하는 훅
- 리액트에서 최적화를 떠올릴때 사용
첫 번째 인수 : 어떠한 값을 반환하는 생성 함수
두 번째 인수 : 해당 함수가 의존하는 값의 배열을 전달함
- useMemo는 렌더링 시 의존성 배열(deps) 안의 값이 변경되지 않았다면 이전에 계산한 값을 그대로 반환하고, 의존성 배열의 값이 변경되면 첫 번째 인수로 전달한 함수가 다시 실행되고, 그 결과값을 반환하며 기억함
import { useMemo } from 'react'
const memoizedValue = useMemo(() => expensiveComputation(a,b), [a,b])
3.1.4 useCallback
useMemo가 값을 기억했다면, useCallback은 인수로 넘겨받은 콜백 자체를 기억함
즉 useCallback은 특정 함수를 새로 만들지 않고 다시 재사용한다는 의미
🌚useCallback ( useMemo와 동일하게 의존성 배열이 변경되지 않는 한 함수를 재생성하지 않음)
첫 번째 인수 : 함수
두 번째 인수 : 의존성 배열
- 함수의 재생성을 막아 불필요한 리소스 또는 리렌더링을 방지하고 싶을 때 사용
🌕useMemo와 useCallback의 차이
- 메모이제이션을 하는 대상이 변수냐 함수냐인 것
- useMemo로 useCallback을 구현하는 경우 불필요하게 코드가 길어지고 혼동을 야기할 수 있으므로 리액트에서 별도로 제공하는 것으로 추측해 볼 수 있음
const handleClick1 = useCallback(() => {
setCounter((prev) => prev + 1);
}, []);
const handleClick2 = useMemo(() => {
return () => setCounter((prev) => prev + 1);
}, []);
💡useCallback 사용 예시
import React, { useState, useCallback } from "react";
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 의존성이 없으므로 처음 한 번만 생성됨
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+</button>
</div>
);
}
export default Counter;
- handleClick 함수는 매 렌더링마다 새로 만들어지지 않음
따라서 이 함수를 props로 넘길 때, 자식 컴포넌트의 불필요한 리렌더링을 막을 수 있음
3.1.5 useRef
- useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장함
🌚useState와 구별되는 큰 차이점
- useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
- useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
useRef로 useState를 흉내내도 렌더링이 되지 않음
🌕 useRef가 왜 필요할까?
- 컴포넌트가 실행되어 렌더링 되지 않았음에도 value라는 값이 기본적으로 존재하게 되는데, 이는 메모리에 불필요한 값을 갖게 하는 악영향을 미침
- 컴포넌트가 여러 번 생성된다면 각 컴포넌트에서 가리키는 값이 모두 value로 동일함
- useRef는 컴포넌트가 렌더링될 때만 생성되며, 컴포넌트 인스턴스가 여러 개라도 각각 별개의 값을 바라봄
🌕 useRef의 가장 일반적인 사용 > DOM에 접근하고 싶을 때
function RefComponent() {
const inputRef = useRef()
// 이땐 렌더링이 실행되기 전으로 undefined를 반환
console.log(inputRef.current) // undefined
useEffect(() => {
console.log(inputRef.current) // <input type="text"></input>
}, [input])
return <input ref={inputRef} type="text" />
}
- useRef는 최초에 넘겨받은 기본값을 가지고 있음
- 여기서 중요한 점은 useRef의 최초 기본값은 return 문에 정의해 둔 DOM이 아닌 useRef()로 넘겨받은 인수인 것!
🌕 useRef를 사용할수 있는 유용한 경우
렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있다는 특징을 활용하여 useState의 이전 값을 저장하는 usePrevious() 같은 훅을 구현할 때 유용함
💡 useRef를 활용한 usePrevious() 훅 구현
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value]) // value가 변경되면 그 값을 ref에 넣음
return ref.current
}
function SomeComponent() {
const [counter, setCounter] = useState(0)
const previousCounter = usePrevious(counter)
function handleClick() {
setCounter((prev) => prev + 1)
}
// 0 (undefined)
// 1, 0
// 2, 1
// 3, 2
return (
<button onClick={handleClick}>
{counter} {previousCounter}
</button>
)
}
개발자가 원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관해주도 싶다면 useRef를 사용하는 것을 추천함!
3.1.6 useContext
🌚Context란
//props 내려주기
<A props={somthing}>
<B props={somthing}>
<C props={somthing}>
<D props={somthing}/>
</C>
</B>
</A>
- props내려주기를 극복하기 위해 등장한 개념으로, 명시적인 props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있음
💡Context를 함수형 컴포넌트에서 사용할 수 있게 해주는 useContext 훅
const Context = createContext<{ hello: string }|undefined>()
function ParentComponent() {
return (
<>
<Context.Provider value={{hello: 'react'}}>
<Context.Provider value={{hello: 'javascript'}}>
<ChildComponent />
</Context.Provider>
</Context.Provider>
</>
)
}
function ChildComponent() {
const value = useContext(Context);
//react가 아닌 javascript가 반환된다.
return <>{value ? value.hello : ''}</>
}
- 상위 컴포넌트에서 만들어진 Context를 함수형 컴포넌트에서 사용할 수 있도록 만들어진 훅
- 상위 컴포넌트 어딘가에서 선언된 <Context.Provider/>에서 제공한 값을 사용할 수 있게 됨
- 컴포넌트 트리가 복잡해질수록 Context를 사용하는 것이 쉽지 않음 (useContext 내부에서 해당 콘텍스트가 존재하는 환경인지, 즉 콘텍스트가 한번이라도 초기화되어 값을 내려주고 있는지 확인해보면 됨)
💡 useContext 사용시 주의할 점
useContext를 함수 컴포넌트 내부에서 사용할 때는 항상 컴포넌트 재활용이 어려워진다는 점을 인지해야 되며, useContext가 선언돼 있으면 Provider에 의존성을 가지고 있는 셈이 되므로 아무데서나 재활용하기에는 어려운 컴포넌트가 됨
해당 함수 컴포넌트가 Provider 하위에 있지 않은 상태로 useContext를 사용한다면 예기치 못한 작동 방식이 만들어지므로 즉, useContext가 있는 컴포넌트는 그 순간부터 눈으로는 직접 보이지도 않을 수 있는 Provider와의 의존성을 갖게 됨
- 이러한 상황을 방지하려면 useContext를 사용하는 컴포넌트를 최대한 작게 하거나 혹은 재사용되지 않을 만한 컴포넌트에서 사용되어야 함
- Context는 상태를 주입해 주는 API이고, 상태 관리 라이브러리가 되기 위해서는 최소한 다음 두 가지 조건을 만족해야 함
- 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 함
- 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 함
- 그러나 Context는 둘 중 어느것도 하지 못하고, 단순히 Props의 값을 하위로 전달해 줄 뿐, useContext를 사용한다고 해서 렌더링이 최적화되지는 않음!
3.1.7 useReducer
- useState의 심화버전
- 상태(state)가 복잡하거나 여러 상태가 하나의 로직으로 묶여야 할 때 사용
- useState처럼 [state, setState]를 반환하지만, 대신 [state, dispatch]를 반환함
state: 현재 상태 값
dispatch: 상태를 바꾸기 위한 함수 (setState 대신 action을 전달)
💡useReducer의 사용법
// useReducer가 사용할 state 정의
type State = {
count: number
}
// state 변화를 발생시킬 action의 타입과 넘겨줄 값(payload)를 정의
// type과 paylooad라는 네이밍을 지킬 필요는 없으며, 객체일 필요도 없음
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, dispatch] = React.useReducer(reducer, initialState, init)
function handleUpButtonClick() {
dispatch({ type: 'up' })
}
function handleDownButtonClick() {
dispatch({ type: 'down' })
}
function handleResetButtonClick() {
dispatch({ type: 'reset', payload: { count: 0 } })
}
return (
<div className="App">
<h1>{state.count}</h1>
<button onClick={handleUpButtonClick}>+</button>
<button onClick={handleDownButtonClick}>-</button>
<button onClick={handleResetButtonClick}>reset</button>
</div>
)
}
💡useReducer의 목적
- 복잡한 형태의 state를 체계적으로 관리하기 위해 사용하고, 미리 정의된 dispatcher로만 state를 변경할 수 있게 만들어, 컴포넌트 내부 로직을 단순화하고 상태 변화 과정을 명확히 함
복잡한 상태(state)를 다룰 때,그 변화를 명확한 규칙(dispatcher와 reducer) 으로 관리하기 위해 사용함
즉, 상태를 바꾸는 방식을 미리 정해둔 함수(reducer) 로 사용하는 것
💡useState와의 비교
useState : 단순한 값(number, boolean 등)을 관리할 때
useReducer: 여러 개의 상태를 함께 관리하거나, 상태 간의 연관성이 있는 복잡한 로직일 경우
3.1.8 useImperativeHandle
- 실제 개발과정에서 자주 볼수 없는 훅
forwardRef가 통로를 만들어주고, useImperativeHandle이 그 통로를 커스터마이징한다.
🌚 React.forwardRef
- ref는 useRef에서 반환한 객체로, 리액트 컴포넌트의 props인 ref에 넣어 HTMLElement에 접근하는 용도로 흔히 사용
- key와 마찬가지로 ref도 리액트에서 컴포넌트의 props로 사용할 수 있는 예약어로 별도 선언되어있지 않더라도 사용할 수 있음
부모 컴포넌트가 자식 컴포넌트 내부의 DOM이나 메서드에 직접 접근할 수 있도록 ref를 전달해주는 함수
기본적으로 ref는 DOM 요소에만 붙일 수 있는데, forwardRef를 사용하면 함수형 컴포넌트에도 ref를 전달할 수 있음
const MyInput = React.forwardRef((props, ref) => {
return <input ref={ref} />;
});
// 부모에서
const inputRef = useRef();
<MyInput ref={inputRef} />;
🌚useImperativeHandle이란
useImperativeHandle(ref, createHandle)
- 부모에게서 넘겨받은 ref를 원하는대로 수정할 수 있는 훅( ref를 통해 노출할 값(또는 메서드)을 커스터마이징하는 Hook )
- forwardRef로 받은 ref에 직접 제어할 수 있는 함수나 값들을 지정할 수 있음
💡useImperativeHandle의 사용법
const Input = forwardRef((props, ref) => {
// useImprerativeHandle를 사용하면 ref의 동작을 추가로 정의할 수 있음
useImperativeHandle(
ref,
() => ({
alert: () => alert(props.value),
}),
// useEffect의 deps와 같음
[props.value],
)
return <input ref={ref} {...props}/>;
});
function App() {
// input에서 사용할 ref
const inputRef = useRef();
// input의 value
const [text, setText] = useState("");
function handleClick() {
// inputRef에 추가한 alert이라는 동작을 사용
inputRef.current.alert();
}
function handleChange(e) {
setText(e.target.value);
}
return (
<>
<Input ref={inputRef} value={text} onChange={handleChange} />
<button onClick={handleClick}>Focus</button>
</>
)
}
💡사용하는 이유
보통 React는 단방향 데이터 흐름을 따르지만, 어떤 경우엔 부모가 자식의 동작을 “명령적으로 제어”해야 할 때가 있음
(예: focus(), scrollToTop(), resetForm() 등)
이때 forwardRef + useImperativeHandle 조합을 쓰면 필요한 기능만 제한적으로 외부에 노출할 수 있음
3.1.9 useLayoutEffect
공식문서에 따르면 아래와 같이 정의되고 있음
이 함수의 시그니처는 useEffect와 동일하나, 모든 DOM의 변경 후에 동기적으로 발생한다.
모든 DOM의 변경 후에 useLayoutEffect의 콜백 함수 실행이 동기적으로 발생한다는 점
🌚useLayoutEffect 실행 순서
1. 리액트가 DOM을 업데이트
2. useLayoutEffect를 실행
3. 브라우저에 변경 사항을 반영
4. useEffect를 실행
- useLayoutEffect가 useEffect보다 먼저 실행되는데, useLayoutEffect가 브라우저에 변경사항이 반영되기 전에 실행되는 반면 useEffect는 브라우저에 변경사항이 반영된 이후에 실행되기 때문임
- 동기적으로 발생한다는 것은 리액트는 useLayoutEffect의 실행이 종료될때까지 기다린 다음에 화면을 그린다는 것을 의미함
- 즉, 리액트 컴포넌트는 useLayoutEffect가 완료될때까지 기다리기 때문에 컴포넌트가 잠시동안 일시정지되는 것같은 일이 발생함
💡useLayoutEffect는 언제 사용하는 것이 좋을까?
DOM은 계산되었지만, 화면이 반영되기 전에 하고 싶은 작업이 있을때와 같이 반드시 필요할 때만 사용것이 좋음
3.1.10 useDebugValue
- 일반적으로 프로덕션 웹서비스에서 사용하는 훅
- 리액트 애플리케이션을 개발하는 과정에서 사용되는데, 디버깅하고 싶은 정보를 해당 훅에 사용하면 리액트 개발자 도구에서 볼수 있음
- 사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅
useDebugValue(value, formatFunction)
//첫 번째 인수: 표시할 값
//두 번째 인수(선택): 값이 표시되기 전 가공하는 함수
3.1.11 훅의 규칙 (Rules of Hooks)
- React에서는 훅을 사용할 때 반드시 지켜야 하는 두 가지 핵심 규칙이 있음
- 이 규칙은 ESLint 플러그인,eslint-plugin-react-hooks의 rules-of-hooks 규칙에서도 강제할 수 있음
1. 최상위에서만 훅을 호출해야함.
반복문, 조건문, 중첩된 함수 안에서 훅을 호출하면 안 되며, 항상 컴포넌트의 최상위 레벨에서만 호출해야 함
즉, 렌더링할 때마다 훅이 항상 동일한 순서로 호출되어야 함.
2. 훅은 오직 React 함수형 컴포넌트나 커스텀 훅 내부에서만 호출할 수 있음 일반 자바스크립트 함수에서는 훅을 쓸 수 없음
function Component() {
const [count, setCount] = useState(0);
const [required, setRequired] = useState(false);
useEffect(() => {
// count 또는 required가 바뀔 때 실행
}, [count, required]);
}'Study > 모던 리액트 Deep Dive' 카테고리의 다른 글
| 모던 리액트 Deep Dive - 서버 사이드 랜더링을 위한 리액트 API (0) | 2025.11.10 |
|---|---|
| 모던 리액트 Deep Dive - 리액트 훅과 고차 컴포넌트 (0) | 2025.11.01 |
| 모던 리액트 Deep Dive - 메모이제이션 (0) | 2025.10.14 |
| 모던 리액트 Deep Dive - 렌더링 (0) | 2025.10.14 |
| 모던 리액트 Deep Dive - 컴포넌트 (0) | 2025.10.07 |
