1. useState
State란 컴포넌트가 가질 수 있는 상태를 의미한다.
useState는 컴포넌트의 상태를 간편하게 생성하고 업데이트 시킬 수 있는 도구를 제공해준다.
// 사용법: const [state, setState] = useState(초기값);
// 아래와 같이 이름을 바꿔 사용할 수 있다.
const [time, setTime] = useState(5);
State를 변경하게 되면, 화면을 다시 렌더링하게 된다.
import { useState } from 'react';
const heavyWork = () => {
console.log('heavy work');
return ['홍길동', '김민수'];
};
function App() {
// 초기값을 가져올 때 무거운 작업을 해야 한다면,
// 값을 넣어주는것이 아닌 callback을 넣어주면 맨 처음에 렌더링될때만 불린다.
const [names, setNames] = useState(() => {
return heavyWork();
});
const [input, setInput] = useState('');
const handleInputChange = (e) => {
setInput(e.target.value);
};
const handleUpload = () => {
// setState를 할 때 이전 상태와 연관이 있다면,
// callback 함수로 해주는 것이 좋다.
setNames((prevState) => {
console.log('prev state: ', prevState);
return [input, ...prevState];
});
};
}
2. useEffect
어떠한 컴포넌트가 Mount(화면에 첫 렌더링), Update(다시 렌더링), Unmount(화면에서 사라질때) 특정 작업을 하고 싶다면 useEffect를 사용한다.
useEffect 함수는 기본적으로 인자로 callback 함수(다른 함수의 인자로 전달된 함수)를 받는다.
import React, { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(1);
const [name, setName] = useState('');
const handleCountUpdate = () => {
setCount(count + 1);
};
const handleInputChange = (e) => {
setName(e.target.value);
};
// 렌더링마다 매번 실행됨 - 렌더링 이후
useEffect(() => {
console.log('렌더링');
});
// 마운팅 + count가 변화할때마다 실행됨
useEffect(() => {
console.log('count 변화');
}, [count]);
// 마운팅 + name이 변경될때마다 실행됨
useEffect(() => {
console.log('name이 변화');
}, [name]);
// 첫 렌더링만 실행
useEffect(() => {
console.log('mount');
}, []);
return (
<div>
<button onClick={handleCountUpdate}>Update</button>
<span>count : {count}</span>
<input type="text" value={name} onChange={handleInputChange}></input>
<span>name : {name}</span>
</div>
);
}
export default App;
Clean Up - 정리
// 함수를 return 해주면
// 컴포넌트가 unmount 될 때,
// 혹은 다음 렌더링 시 불릴 useEffect가 불리기 전에 실행된다
useEffect(() => {
// 구독
return () => {
// 구독 해지
}
}, []);
// Timer.jsx
import React, { useEffect } from 'react';
const Timer = (props) => {
// dependency array가 비어있으므로 mount 될때만 실행
useEffect(() => {
const timer = setInterval(() => {
console.log('timer is running...');
}, 1000);
// clean-up: unmount 될 때 실행
return () => {
clearInterval(timer);
console.log('timer is stopped');
};
}, []);
return (
<div>
<span>Timer start!</span>
</div>
);
};
export default Timer;
import React, { useState, useEffect } from 'react';
import Timer from './components/Timer';
function App() {
const [showTimer, setShowTimer] = useState(false);
return (
<div>
{ /* Timer 컴포넌트를 state 값에 따라 보여준다 */ }
{showTimer && <Timer />}
{ /* 버튼을 클릭하면 state 값을 전환해줌 */ }
<button onClick={() => setShowTimer(!showTimer)}>Toggle Timer</button>
</div>
);
}
export default App;
3. useRef
const ref = useRef(value);
// 위와 같이 ref를 사용하면 { current: value } 로 값이 저장된다.
ref가 유용한 2가지 상황
1. 저장공간
State의 변화 -> 렌더링 -> 컴포넌트 내부 변수들 초기화
: State가 변하면 컴포넌트가 다시 렌더링 되기 때문에 내부 변수들이 다시 초기화가 된다.
Ref의 변화 -> No 렌더링 -> 변수들의 값이 유지됨
: Ref의 값이 변해도 다시 렌더링되지 않기 때문에 변수들의 값이 유지된다.
또한 컴포넌트가 다시 렌더링 되어도 Ref안의 값은 변하지 않고 유지가 된다.
import React, { useState, useRef } from 'react';
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
console.log('rendering...');
const increaseCountState = () => {
setCount(count + 1);
}
const increaseCountRef = () => {
countRef.current = countRef.current + 1;
console.log('Ref: ', countRef.current);
}
return (
<div>
<p>State: {count}</p>
<p>Ref: {countRef.current}</p>
<button onClick={increaseCountState}>Increase State</button>
<button onClick={increaseCountRef}>Increase Ref</button>
</div>
);
}
export default App;
Increase State 버튼을 누르면 누를때마다 화면이 re-render가 되기 때문에 값이 1씩 증가하는것이 보인다.
하지만 Increase Ref 버튼을 누르면 값이 증가하는것이 보이지 않는다.
Ref는 값이 변해도 화면이 re-render가 되지 않기 때문에 화면에 보이지 않을뿐 값은 증가하고 있다.
따라서 다시 Increase State 버튼을 누른다면 화면이 re-render가 되기 때문에 증가한 Ref 값을 볼 수 있다.
2. DOM 요소에 접근
대표적으로 input 요소를 클릭하지 않아도 focus를 주고 싶을때 많이 사용된다.
import React, { useState, useRef, useEffect } from 'react';
function App() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
const login = () => {
alert(`Welcome ${inputRef.current.value}`);
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" placeholder="username" />
<button onClick={login}>로그인</button>
</div>
);
}
export default App;
맨 처음에 화면이 rendering 될 때 useEffect가 실행되어 input에 focus가 잡히게 된다.
그리고 login 버튼을 누르면 onClick에 등록된 함수가 실행되어 alert 창이 뜨고 다시 focus가 잡히도록 했다.
이처럼 태그에 property로 ref를 이용해서 useRef로 만든 ref를 등록해주면 DOM 요소에 접근할 수 있다.
장점
우리가 정말 자주 변하는 값을 Ref가 아닌 State에 넣어놨다고 해보자.
그러면 계속해서 re-render가 일어나기 때문에 성능에 좋지 않은 영향을 끼칠것이다.
하지만 useRef를 사용한다면 값이 변해도 컴포넌트가 re-render가 되지 않기 때문에 그럴일은 없다.
import React, { useState, useRef } from 'react';
function App() {
const [renderer, setRenderer] = useState(0);
const countRef = useRef(0);
let countVar = 0;
const doRendering = () => {
setRenderer(renderer + 1);
}
const increaseRef = () => {
countRef.current = countRef.current + 1;
console.log('ref: ', countRef.current);
}
const increaseVar = () => {
countVar = countVar + 1;
console.log('var: ', countVar);
}
return (
<div>
<p>Ref: {countRef.current}</p>
<p>Var: {countVar}</p>
<button onClick={doRendering}>Render!</button>
<button onClick={increaseRef}>Increase Ref</button>
<button onClick={increaseVar}>Increase Var</button>
</div>
);
}
export default App;
State가 변하면 화면이 re-render가 되기 때문에 State 값을 변화시켜 re-render를 해주는 버튼을 하나 만들었다.
Ref와 Var 각각 3번씩 눌러주고 Render! 버튼을 누른다면 어떻게 될까?
Ref는 정상적으로 3이 출력되지만 Var는 0 그대로이다.
그 이유는 Ref는 전 생애주기를 통해서 유지되기 때문에 컴포넌트가 다시 렌더링되어도 초기화가 되지 않는다.
하지만 countVar 변수는 컴포넌트가 re-render가 되면 다시 0으로 초기화가 되기 때문이다.
(Ref가 전 생애주기를 통해서 값이 유지가 된다는것은, mount 된 후부터 unmount가 되기 전까지 값을 계속 유지한다는 것을 의미한다.)
예시
import React, { useState, useRef, useEffect } from 'react';
function App() {
const [count, setCount] = useState(1);
const [renderCount, setRenderCount] = useState(1);
useEffect(() => {
console.log('rendering!');
setRenderCount(renderCount + 1);
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
</div>
);
}
export default App;
State를 1씩 증가시키는 버튼, 함수가 있고 그 버튼을 눌렀을 때 렌더링이 몇번 되는지 확인하고 싶어서 위와 같이 코드를 짰다고 해보자. useEffect가 렌더링 될 때마다 실행되기에 이렇게 짜면 될 것 같다.
하지만 아래와 같은 엄청난 양의 렌더링과 함께 call stack을 초과해버린다. (무한루프)
위와같은 현상이 발생하게 된 이유는 무엇일까?
- 일단 우리는 State의 값을 올리기 위해 setCount()를 실행한다.
- State가 변했으므로 컴포넌트가 re-render가 되고 useEffect()가 불린다.
- useEffect안의 setRenderCount()가 실행되고, State 값이 변한다.
- State값이 변했기 때문에 re-render가 된다.
- 2번 무한반복...
위와 같은 상황을 피하기 위해선 아래와같이 useState 대신 useRef를 사용하면 된다.
import React, { useState, useRef, useEffect } from 'react';
function App() {
const [count, setCount] = useState(1);
const renderCount = useRef(1);
useEffect(() => {
renderCount.current = renderCount.current + 1;
console.log('rendering count: ', renderCount.current);
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
</div>
);
}
export default App;
만약 화면을 처음 렌더링한다면 Ref의 값은 1이 아닌 2가 될 것이다.
왜냐하면 useEffect가 렌더링시에 실행되는데 처음 컴포넌트가 보여지는 것도 렌더링이기 때문에 한번 실행되기 때문이다.
useRef는 re-render를 발생시키지 않기 때문에 State를 사용한 예시와 달리 무한루프에 빠지지 않게 할 수 있다.
결론
useRef는 변화는 감지해야 하지만, 그 변화가 rendering을 발생시키면 안되는 어떠한 값을 다룰 때 편리하다.
4. useContext
수많은 컴포넌트를 지닌 리액트 앱은 수많은 컴포넌트들이 공통적으로 필요한 전역적인 데이터가 있을 수 있다.
예시로 현재 로그인된 사용자 정보, 테마, 언어 등을 예로 들 수 있겠다.
이런 전역적인 데이터들을 부모에서 자식으로 props로 단계적으로 계속 전달을 해줘야 한다면 엄청난 props drilling에 빠질 수 있다. 이럴 경우의 단점은 코드가 바뀌면 관련된 컴포넌트 역시 다 수정해줘야 한다는 것이다.
그래서 리액트는 이러한 문제점을 해결하기 위해 전역적인 데이터들을 쉽게 관리하도록 Context를 제공해준다.
Context를 사용하면 props로 자식 컴포넌트에게 일일히 전달해주지 않고 broadcasting을 해준다.
그러면 전부 Context를 사용하면 되지 props를 쓸 필요가 없는거 아닌가? 라고 생각할 수 있다.
Context는 꼭 필요할때만!
- Context를 사용하면 컴포넌트를 재사용하기 어려워 질 수 있다
- Props drilling을 피하기 위한 목적이라면 Component Composition(컴포넌트 합성)을 먼저 고려해보자
Context를 사용하지 않은 예시
import React, { useState } from 'react';
import './App.css';
import Page from './components/Page';
function App() {
const [isDark, setIsDark] = useState(false);
return <Page isDark={isDark} setIsDark={setIsDark} />
}
export default App;
import React from 'react';
const Content = ({ isDark }) => {
return (
<div
className="content"
style={{
backgroundColor: isDark ? 'black' : 'white',
color: isDark ? 'white' : 'black',
}}
>
<p>Good day</p>
</div>
);
};
export default Content;
import React from 'react';
const Footer = ({ isDark, setIsDark }) => {
const toggleTheme = () => {
setIsDark(!isDark);
};
return (
<footer
className="footer"
style={{ backgroundColor: isDark ? 'black' : 'lightgray' }}
>
<button className="button" onClick={toggleTheme}>
Dark Mode
</button>
</footer>
);
};
export default Footer;
import React from 'react';
const Header = ({ isDark }) => {
return (
<header
className="header"
style={{
backgroundColor: isDark ? 'black' : 'lightgrey',
color: isDark ? 'white' : 'black',
}}
>
<h1>Welcome!</h1>
</header>
);
};
export default Header;
import React from 'react';
import Header from './Header';
import Content from './Content';
import Footer from './Footer';
const Page = ({ isDark, setIsDark }) => {
return (
<div className="page">
<Header isDark={isDark} />
<Content isDark={isDark} />
<Footer isDark={isDark} setIsDark={setIsDark} />
</div>
);
};
export default Page;
다크모드와 관련된 데이터인 state를 자식 컴포넌트로 props를 통해서 계속 전달해준다.
Context를 사용한 예시
import React, { useState } from 'react';
import './App.css';
import Page from './components/Page';
import { ThemeContext } from './context/ThemeContext';
function App() {
const [isDark, setIsDark] = useState(false);
return (
<ThemeContext.Provider value={{ isDark, setIsDark }}>
<Page />
</ThemeContext.Provider>
);
}
export default App;
import React, { useContext } from 'react';
import { ThemeContext } from '../context/ThemeContext';
const Content = () => {
const { isDark } = useContext(ThemeContext);
return (
<div
className="content"
style={{
backgroundColor: isDark ? 'black' : 'white',
color: isDark ? 'white' : 'black',
}}
>
<p>Good day</p>
</div>
);
};
export default Content;
import React, { useContext } from 'react';
import { ThemeContext } from '../context/ThemeContext';
const Footer = () => {
const { isDark, setIsDark } = useContext(ThemeContext);
const toggleTheme = () => {
setIsDark(!isDark);
};
return (
<footer
className="footer"
style={{ backgroundColor: isDark ? 'black' : 'lightgray' }}
>
<button className="button" onClick={toggleTheme}>
Dark Mode
</button>
</footer>
);
};
export default Footer;
import React, { useContext } from 'react';
import { ThemeContext } from '../context/ThemeContext';
const Header = () => {
const { isDark } = useContext(ThemeContext);
return (
<header
className="header"
style={{
backgroundColor: isDark ? 'black' : 'lightgrey',
color: isDark ? 'white' : 'black',
}}
>
<h1>Welcome!</h1>
</header>
);
};
export default Header;
import React, { useContext } from 'react';
import Header from './Header';
import Content from './Content';
import Footer from './Footer';
const Page = () => {
return (
<div className="page">
<Header />
<Content />
<Footer />
</div>
);
};
export default Page;
가장 최상위 컴포넌트에서 Provider로 감싸주면 자식 컴포넌트에서 필요한것만 접근할 수 있다.
5. useMemo
useMemo에서 Memo는 Memoization을 뜻한다. Memoization이란 동일한 값을 반복적으로 호출해야하는 함수가 있다면, 맨 처음에 그 값을 계산할때 메모리에 저장해놨다가 필요할때마다 다시 계산하지 않고 메모리에서 꺼내서 재사용하는 기법을 말한다.
function calculate() {
return 10;
}
function Component() {
const value = calculate();
return <div>{ value }</div>
}
위와 같은 코드가 있다고 해보자. 함수형 컴포넌트가 렌더링 된다는 말은 Component 함수를 호출하고, 그 컴포넌트 내의 모든 변수가 초기화 된다는것을 의미한다. 그러면 위 코드에서는 value가 계속 초기화가 되기 때문에 렌더링이 될 때마다 calculate 함수가 계속 불릴것이다.
지금은 calculate 함수가 단순하지만 무거운 함수라면 렌더링 될 때마다 계속 불리기 때문에 비효율적일 것이다.
하지만 아래와 같이 useMemo를 사용한다면 해결할 수 있다.
function calculate() {
return 10;
}
function Component() {
const value = useMemo(
() => calculate(), []
);
return <div>{ value }</div>
}
위와 같이 useMemo를 사용하게 되면 처음에 계산된 결과값을 메모리에 저장해서 컴포넌트가 반복적으로 렌더링이 되어도 calculate를 계속 다시 호출하지 않고 이전에 메모리에 저장된 결과값을 꺼내와서 사용한다.
useMemo의 구조
const value = useMemo(() => {
return calculate();
}, [item]);
첫번째 인자인 callback 함수는 메모이제이션을 해줄 값을 계산해서 return 해준다.
두번째 인자인 배열은 의존성 배열으로, 배열안의 요소의 값이 update 될 때만 callback 함수를 다시 호출해서 메모이제이션 된 값을 update 해서 다시 메모이제이션을 해준다.
만약 빈 배열을 넣어준다면 맨 처음 컴포넌트가 mount 되었을 때만 값을 계산하고 이후에는 항상 메모이제이션 된 값을 꺼내와서 사용한다.
useMemo, 꼭 필요할때만!
useMemo를 사용한다는것은 값을 재활용하기 위해서 따로 메모리를 소비해서 저장을 한다는것을 의미한다.
그렇기 때문에 불필요한 값까지 모두 메모이제이션을 하면 성능이 악화될 수 있다.
예시1
import React, { useState, useMemo } from 'react';
const hardCalculate = (number) => {
console.log('hard calculate!');
for (let i = 0; i < 999999999; i += 1) {}
return number + 10000;
};
function App() {
const [hardNumber, setHardNumber] = useState(1);
const hardSum = hardCalculate(hardNumber);
return (
<div>
<h3>Hard Calculator</h3>
<input
type="number"
value={hardNumber}
onChange={(e) => setHardNumber(parseInt(e.target.value))}
/>
<span>+ 10000 = {hardSum} </span>
</div>
);
}
export default App;
위와 같이 시간이 오래걸리는 함수를 사용한다고 해보자. 실제로 반복문에서 수행하는 로직은 없지만 input의 값이 변할때마다 반복문이 수행되기 때문에 값이 변할때 delay가 생긴다.
그래서 위처럼 오래걸리는 함수가 아닌 쉬운 계산기를 아래와 같이 추가로 만들어보았다.
import React, { useState, useMemo } from 'react';
const hardCalculate = (number) => {
console.log('hard calculate!');
for (let i = 0; i < 999999999; i += 1) {}
return number + 10000;
};
const easyCalculate = (number) => {
console.log('easy calculate!');
for (let i = 0; i < 999999999; i += 1) {}
return number + 1;
};
function App() {
const [hardNumber, setHardNumber] = useState(1);
const [easyNumber, setEasyNumber] = useState(1);
const hardSum = hardCalculate(hardNumber);
const easySum = easyCalculate(easyNumber);
return (
<div>
<h3>Hard Calculator</h3>
<input
type="number"
value={hardNumber}
onChange={(e) => setHardNumber(parseInt(e.target.value))}
/>
<span>+ 10000 = {hardSum} </span>
<h3>Easy Calculator</h3>
<input
type="number"
value={easyNumber}
onChange={(e) => setEasyNumber(parseInt(e.target.value))}
/>
<span>+ 1 = {easySum} </span>
</div>
);
}
export default App;
Easy Caculator의 input값을 바꾸면 단순히 1을 더해서 return 해주는 함수를 사용하기 때문에 굉장히 빠를 것 같다.
하지만 실제로는 아래와 같은 이유때문에 그렇지 않다.
- Easy Calculator의 input 값을 변화시킨다.
- onChange의 setEasyNumber가 불린다.
- setEasyNumber에 state인 easyNumber가 전달된다.
- easyNumber의 값이 변한다.
- state가 변했으므로 App 컴포넌트가 re-render 된다.
- 컴포넌트가 rendering 됐으므로 모든 변수가 초기화가 된다.
- hardSum이란 변수에서 시간이 오래걸리는 hardCalculate를 부르기 때문에 시간이 오래걸린다.
따라서 우리는 직접 hardCalculate를 사용하지 않았음에도 state의 변화로 인한 컴포넌트의 re-rendering에 따라서 불리기 때문에 시간이 오래걸린다.
따라서 아래와 같이 바꿔주면 된다.
...
const hardSum = useMemo(() => {
return hardCalculate(hardNumber);
}, [hardNumber]);
...
위와 같이 바꿔주게 되면 의존성배열에 들어간 hardNumber가 바뀌었을때만 hardCalculate()가 실행이된다.
따라서 easyNumber의 값을 바꿔주어도 더이상 hardNumber는 실행되지 않는다.
하지만 사실 React로 개발하면서 위처럼 오래 걸리는 함수를 사용할 일이 거의 없을 것이다.
아래와 같은 자주 사용할 수 있는 예시로 다시 익혀보자.
예시2
import React, { useState, useMemo, useEffect } from 'react';
function App() {
const [number, setNumber] = useState(0);
const [isKorea, setIsKorea] = useState(true);
const location = isKorea ? 'korea' : 'foreign';
useEffect(() => {
console.log('Call useEffect');
}, [location]);
return (
<div>
<h2>How many meals do you eat a day?</h2>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<hr />
<h2>what country are you in?</h2>
<p>country: {location}</p>
<button onClick={() => setIsKorea(!isKorea)}>get on a plane</button>
</div>
);
}
export default App;
버튼을 누르면 'korea'와 'foreign'가 왔다갔다 거리는 간단한 컴포넌트이다.
그리고 useEffect를 이용해서 location이 바뀔 때만 불리도록 해주었다. 따라서 number가 바뀌어도 useEffect는 불리지 않을 것이다.
지금처럼 의존성 배열에 들어간 값이 문자열처럼 원시 타입이라면 괜찮지만 객체같은 참조 타입이라면 원하는대로 동작하지 않을 수 있다.
import React, { useState, useMemo, useEffect } from 'react';
function App() {
const [number, setNumber] = useState(0);
const [isKorea, setIsKorea] = useState(true);
const location = { country: isKorea ? 'korea' : 'foreign' };
useEffect(() => {
console.log('Call useEffect');
}, [location]);
return (
<div>
<h2>How many meals do you eat a day?</h2>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<hr />
<h2>what country are you in?</h2>
<p>country: {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>get on a plane</button>
</div>
);
}
export default App;
이번엔 location을 객체 타입으로 바꾸어주었다. 그리고 { location } 또한 { location.country } 로 바꿔주었다.
그런데 이번엔 number를 바꿔도 useEffect가 호출이 된다. 그 이유는 객체 타입에 있다.
객체는 값이 같더라도 메모리에는 참조 주소값이 들어가있고, 두 객체는 다른 주소를 가리키기 때문이다.
따라서 number라는 state가 바뀌어서 App 컴포넌트가 re-render가 되고, location 또한 다시 초기화가 될 것이다.
값은 같지만 이전의 객체와 현재 새로운 객체의 주소가 다르기 때문에 useEffect가 불리게 되는것이다.
그래서 useMemo를 이용해서 아래와 같이 최적화 시킬 수 있다.
import React, { useState, useMemo, useEffect } from 'react';
function App() {
const [number, setNumber] = useState(0);
const [isKorea, setIsKorea] = useState(true);
const location = useMemo(() => {
return { country: isKorea ? 'korea' : 'foreign' };
}, [isKorea]);
useEffect(() => {
console.log('Call useEffect');
}, [location]);
return (
<div>
<h2>How many meals do you eat a day?</h2>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<hr />
<h2>what country are you in?</h2>
<p>country: {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>get on a plane</button>
</div>
);
}
export default App;
위와 같이하면 더이상 number state가 변하더라도 isKorea가 바뀔 때만 location.country의 값이 변하기 때문에 useEffect가 불리지 않게 될 것이다.
6. useCallback
useCallback은 useMemo와 마찬가지로 memoization 기법을 사용한다. 하지만 다른점이라면 useMemo는 안의 callback 함수가 return 해준 값을 memoization 하지만, useCallback은 대신에 인자로 전달한 callback 함수 그 자체를 memoization 해준다.
function Component() {
const calculate = (num) => {
return num + 1;
}
return <div>{ value }</div>
}
위의 코드에서 Component가 렌더링되면 Component 내의 모든 변수들은 초기화 될 것이다.
따라서 또한 calculate 함수 또한 초기화 될 것이다. 그 말은 Component가 렌더링 될 때마다 새로 만들어진 calculate 함수를 매번 할당받는다는 의미이다.
function Component() {
const calculate = useCallback((num) => {
return num + 1;
}, [item]);
return <div>{ value }</div>
}
하지만 위와 같이 useCallback을 이용해준다면 calculate 함수 자체를 memoization 해주기 때문에 매번 새로 할당받을 필요없이 메모리에 저장되어 있는 함수를 가져다 쓰면 된다. 따라서 컴포넌트가 다시 렌더링이 되더라도 calculate가 초기화 되는것을 막을 수 있다.
useCallback의 구조
useCallback(() => {
return value;
}, [item]);
위의 hook 들을 봐왔다면 익숙한 구조이다. useCallback의 첫 번째 인자로는 memoization할 callback 함수가 들어가고, 두 번째 인자로는 의존성 배열이 들어간다.
여태 봐왔던것과 마찬가지로 의존성배열 안의 값이 변해야 실행이되고, 빈 배열을 주면 맨 처음 렌더링 될때만 실행이 된다.
예시1
import { useEffect, useState } from 'react';
function App() {
const [number, setNumber] = useState(0);
const someFunction = () => {
console.log(`someFunc: number: ${number}`);
return;
};
useEffect(() => {
console.log('someFunction is changed');
}, [someFunction])
return (
<div>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<br />
<button onClick={someFunction}>Call someFunc</button>
</div>
);
}
export default App;
input창과 button을 만들어 input의 값이 바뀔때마다 number state를 바꿔주고, button을 누르면 현재 number state의 값을 보여주는 컴포넌트이다. 그리고 useEffect를 통해서 의존성 배열에 현재 number state를 보여주는 함수인 someFunction을 넣어서 함수가 바뀔때마다 useEffect를 실행하도록 하였다.
함수는 변할일이 없기 때문에 언뜻보면 useEffect는 호출되지 않을 것 같다. 하지만 state의 값이 바뀔때마다, 즉 state의 값이 바뀌어 컴포넌트가 re-render가 될 때마다 useEffect는 아래와같이 계속 호출이된다.
이렇게 계속 호출이 되는 이유는 사실 코드를 조금만 들여다보면 알 수 있다.
전에 설명했듯이, 객체는 안의 값이 같더라도 다른 메모리주소에 할당되기 때문에 다른 객체로 취급이된다.
...
const someFunction = () => {
console.log(`someFunc: number: ${number}`);
return;
};
...
위의 예제에서 사용한 코드를 보면, 함수는 {}로 감싸져 있는 것을 볼 수 있다. someFunction도 const로 생성된 하나의 변수이다. 그리고 그 변수엔 {}로 둘러쌓인 하나의 함수 '객체'를 가지고 있는 것이다. 따라서 컴포넌트가 re-render가 될 때마다 someFunction이 초기화가 되고, 함수 내용은 같지만 또 다른 객체가 생성되면서 useEffect의 의존성배열에 넣어준 someFunction이 달라졌다고 판단해 useEffect가 계속 불리는것이다.
따라서 위의 함수 자체를 아래와 같이 memoization을 해놓는다면 렌더링이 될 때마다 새로 생성하는것이 아닌 메모리에서 똑같은 객체를 가져다 쓰므로, useEffect는 불리지 않게 될 것이다.
import { useCallback, useEffect, useState } from 'react';
function App() {
const [number, setNumber] = useState(0);
const someFunction = useCallback(() => {
console.log(`someFunc: number: ${number}`);
return;
}, []);
useEffect(() => {
console.log('someFunction is changed');
}, [someFunction])
return (
<div>
<input
type="number"
value={number}
onChange={(e) => setNumber(e.target.value)}
/>
<br />
<button onClick={someFunction}>Call someFunc</button>
</div>
);
}
export default App;
하지만 위의 코드에서도 문제점이 생긴다. 바로 Call someFunc 버튼을 눌러 state 값을 출력하고자 할 때이다.
위와 같이 input의 값을 5까지 올렸음에도 불구하고 버튼을 누르면 0의 값이 출력된다.
이유는 단순하다. 우리가 someFunction 함수를 memoization 해서 저장할 당시에 number state의 값이 0이기 때문이다. 우리는 number state의 값이 0인 함수를 계속 재사용하므로 변하지 않은 값이 출력이 되는것이다.
위와 같은 상황을 해결하려면 단순하다. 아래와같이 지금까지 계속 그래왔듯 의존성 배열안에 number state를 넣어주어서, number state의 값이 바뀔때마다 useCallback을 이용해서 memoization 시켜주면 된다.
...
const someFunction = useCallback(() => {
console.log(`someFunc: number: ${number}`);
return;
}, [number]);
...
그러면 위와같이 우리가 의도한대로 정상 출력이 되는것을 볼 수 있다!
예시2
import React, { useState } from 'react';
import Box from './Box';
function App() {
const [size, setSize] = useState(100);
const createBoxStyle = () => {
return {
backgroundColor: 'pink',
width: `${size}px`,
height: `${size}px`,
};
};
return (
<div>
<input
type="number"
value={size}
onChange={(e) => setSize(e.target.value)}
/>
<Box createBoxStyle={createBoxStyle} />
</div>
);
}
export default App;
style 자체를 객체로 만들어서 prop으로 Box 컴포넌트에 전달해주는 것을 볼 수 있다.
import React, { useEffect, useState } from 'react';
const Box = ({ createBoxStyle }) => {
const [style, setStyle] = useState({});
useEffect(() => {
console.log('box size UP!');
setStyle(createBoxStyle());
}, [createBoxStyle]);
return <div style={style}></div>
}
export default Box;
Box 컴포넌트에선 전달받은 style 객체인 createBoxStyle을 div에 적용해주어서 return해주는 것을 볼 수 있다.
그리고 그 객체가 바뀔때마다 useEffect가 실행되어서 style이 바뀌고 div에 적용한다.
추가로 검정색 박스를 나타났다 사라졌다 하는 Change Theme 버튼을 하나 더 넣어봤다.
import React, { useState } from 'react';
import Box from './Box';
function App() {
const [size, setSize] = useState(100);
const [isDark, setIsDark] = useState(false);
const createBoxStyle = () => {
return {
backgroundColor: 'pink',
width: `${size}px`,
height: `${size}px`,
};
};
return (
<div>
<input
type="number"
value={size}
onChange={(e) => setSize(e.target.value)}
/>
<button onClick={() => setIsDark(!isDark)}>Change Theme</button>
<Box createBoxStyle={createBoxStyle} />
<div
style={{
background: isDark ? 'black' : 'white',
width: '100px',
height: '100px',
}}
></div>
</div>
);
}
export default App;
핑크색 박스와, 즉 Box 컴포넌트와 관계없는 일임에도 불구하고 Change Theme을 누르면 Box 컴포넌트에 있는 useEffect가 실행되어 box size up!이 출력된다. 바로 App.js에 있는 createBoxStyle이 계속 초기화가 되기 때문이다.
위 현상 역시 해결하려면 useCallback을 이용해주면 된다.
...
const createBoxStyle = useCallback( () => {
return {
backgroundColor: 'pink',
width: `${size}px`,
height: `${size}px`,
};
}, [size]);
...
위와 같이 더 이상 useEffect가 실행되지 않는 것을 볼 수 있다.
7. useReducer
useReducer는 useState 처럼 State를 생성하고 관리할 수 있게 해주는 도구이다.
하지만 복잡한 값을 가진 State를 관리해야 한다면 useState 대신 useReducer를 사용하면 훨씬 깔끔하게 관리할 수 있다.
구성 요소
- Dispatch: Reducer에게 전해지는 어떠한 '요구를 하는 그 행위'를 말한다.
- Action: Reducer에게 전해지는 '요구 내용'이다.
- Reducer: 요구를 받아서 State를 Update 시켜주는 주체이다.
실행 순서
- Dispatch 라는 함수 안에 Action을 넣어서 Reducer에게 전달한다.
- Reducer는 Action의 내용대로 State를 Update 한다.
예시
import React, { useReducer, useState } from 'react';
// reducer - state를 업데이트 하는 역할(은행)
// dispatch - state 업데이트를 위한 요구
// action - 요구의 내용
const ACTION_TYPES = {
deposit: 'deposit',
withdraw: 'withdraw',
};
// state와 해야 할 action을 인자로 받는다
const reducer = (state, action) => {
console.log('reducuer is working', state, action);
switch (action.type) {
case ACTION_TYPES.deposit:
return state + action.payload;
case ACTION_TYPES.withdraw:
return state - action.payload;
default:
return state;
}
};
function App() {
const [number, setNumber] = useState(0);
const [money, dispatch] = useReducer(reducer, 0);
return (
<div>
<h2>useReducer 은행에 오신것을 환영합니다.</h2>
<p>잔고: {money}원</p>
<input
type="number"
value={number}
onChange={(e) => setNumber(parseInt(e.target.value))}
step="1000"
/>
<button
onClick={() => {
dispatch({ type: ACTION_TYPES.deposit, payload: number });
}}
>
예금
</button>
<button
onClick={() => {
dispatch({ type: ACTION_TYPES.withdraw, payload: number });
}}
>
출금
</button>
</div>
);
}
export default App;
예시2
import React, { useReducer, useState } from 'react';
import Student from './Student';
const reducer = (state, action) => {
switch (action.type) {
case 'add-student':
const name = action.payload.name;
const newStudent = {
id: Date.now(),
name,
isHere: false,
};
return {
count: state.count + 1,
students: [...state.students, newStudent],
};
case 'delete-student':
return {
count: state.count - 1,
students: state.students.filter(
(student) => student.id !== action.payload.id
),
};
case 'mark-student':
return {
count: state.count,
students: state.students.map((student) => {
if (student.id === action.payload.id) {
return { ...student, isHere: !student.isHere };
}
return student;
}),
};
default:
return state;
}
};
const initialState = {
count: 0,
students: [],
};
function App() {
const [name, setName] = useState('');
const [studentInfo, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h1>출석부</h1>
<p>총 학생 수: {studentInfo.count}</p>
<input
type="text"
placeholder="이름을 입력해주세요"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button
onClick={() => dispatch({ type: 'add-student', payload: { name } })}
>
추가
</button>
{studentInfo.students.map((student) => {
return (
<Student
key={student.id}
name={student.name}
dispatch={dispatch}
id={student.id}
isHere={student.isHere}
/>
);
})}
</div>
);
}
export default App;
import React from 'react';
const Student = ({ name, dispatch, id, isHere }) => {
return (
<div>
<span
style={{
textDecoration: isHere ? 'line-through' : 'none',
color: isHere ? 'gray' : 'black',
}}
onClick={() => {
dispatch({ type: 'mark-student', payload: { id } });
}}
>
{name}
</span>
<button
onClick={() => {
dispatch({ type: 'delete-student', payload: { id } });
}}
>
삭제
</button>
</div>
);
};
export default Student;
학생의 이름을 클릭하면 글씨가 회색으로 변하고 중간에 line-through가 쳐진다. 그리고 삭제 버튼을 누르면 리스트에서 학생의 이름이 지워지고 총 학생 수가 하나 감소된다. 추가는 리스트에 학생이름이 추가되고 총 학생 수가 하나 증가한다.
확실히 모든 action을 reducer 한 곳에서 관리할 수 있으니 편리하다. State가 객체 배열같은 복잡한 구조임에도 불구하고 각각의 상황에 따라서 같은 State에 대한 관리를 한 곳에서 관리할 수 있다는점. 그것이 useReducer의 장점이 아닌가 싶다. 한번에 이해하긴 어려운 개념으로, 계속해서 보고 이해를 해야 할 것 같다.
8. React.memo
리액트에서는 기본적으로 부모 컴포넌트가 렌더링되면 모든 자식 컴포넌트들도 렌더링이 된다.
하지만 자식 컴포넌트가 항상 고정된 값을 props로 전달받는 컴포넌트이고, 항상 화면에 같은 결과를 렌더링하는 컴포넌트라면 부모컴포넌트가 렌더링 될 때마다 굳이 또 렌더링이 될 필요가 없다.
이럴때 React.memo를 사용하면 된다. React.memo는 React에서 제공하는 고차 컴포넌트이다.(HOC: Higher Order Component)
HOC란 어떤 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환해주는 컴포넌트이다.
기능이나 UI적으로 바뀌는게 없지만 최적화된 컴포넌트를 반환해준다. props check를 통해 props에 변화가 있다면 다시 렌더링해주고, 변화가 없다면 기존에 이미 렌더링이 된 내용을 재사용한다.
React.memo 꼭 필요할때만!
무분별하게 사용한다면 오히려 성능에 독이 될 수도 있다. 왜냐하면 memoization 할 때 결국 메모리 어딘가에 저장을 해놓아야 하는데, 모든 곳에 React.memo를 사용한다면 결국 메모리를 추가로 사용하기 때문이다.
- 컴포넌트가 같은 Props로 자주 렌더링 될때
- 컴포넌트가 렌더링이 될때마다 복잡한 로직을 처리해야될때
위 두가지 경우를 빼고는 과연 써야하는지 고민해보는것이 좋다.
주의점
React.memo는 오직 Props에만 의존해서 렌더링을 결정한다.
따라서 useState, useReducer, useContext 등 상태변화가 일어난다면 props의 변화가 없더라도 re-render가 발생한다.
예시1 (React.memo)
import React, { useState } from 'react';
import Child from './Child';
function App() {
const [parentAge, setParentAge] = useState(0);
const [childAge, setChildAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
const incrementChildAge = () => {
setChildAge(childAge + 1);
}
console.log('Parent Component is rendered');
return (
<div style={{ border: '2px solid navy', padding: '10px' }}>
<h1>Parent</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>Increment Parent Age</button>
<button onClick={incrementChildAge}>Increment Child Age</button>
<Child name={'홍길동'} age={childAge} />
</div>
);
}
export default App;
import React from 'react';
const Child = ({name, age}) => {
console.log('Child Component is rendered');
return (
<div style={{ border: '2px solid powderblue', padding: '10px' }}>
<h3>Child</h3>
<p>name: {name}</p>
<p>age: {age}</p>
</div>
);
};
export default Child;
Child 컴포넌트에 전달되는 age의 값은 바뀌지 않았음에도 불구하고 부모 컴포넌트가 re-render 되었기 때문에 같이 렌더링되었다. 이렇게 자식 컴포넌트는 변하는것이 없음에도 불구하고 렌더링될경우 React.memo를 사용해주면 좋다.
import React, { memo } from 'react';
const Child = ({name, age}) => {
console.log('Child Component is rendered');
return (
<div style={{ border: '2px solid powderblue', padding: '10px' }}>
<h3>Child</h3>
<p>name: {name}</p>
<p>age: {age}</p>
</div>
);
};
export default memo(Child);
자식 컴포넌트인 Child 컴포넌트에서 간단히 memo만 추가시켰다. 그러면 아래와 같은 결과를 얻을 수 있다.
화면에 처음으로 렌더링 될 때 빼고는 자식 컴포넌트는 부모 컴포넌트가 렌더링 되었음에도 불구하고 다시 렌더링되지 않았다.
예시2 (React.memo + useMemo)
import React, { useState } from 'react';
import Child from './Child';
function App() {
const [parentAge, setParentAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
console.log('Parent Component is rendered');
const name = {
lastName: '홍',
firstName: '길동',
};
return (
<div style={{ border: '2px solid navy', padding: '10px' }}>
<h1>Parent</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>Increment Parent Age</button>
<Child name={name} />
</div>
);
}
export default App;
import React, { memo } from 'react';
const Child = ({name}) => {
console.log('Child Component is rendered');
return (
<div style={{ border: '2px solid powderblue', padding: '10px' }}>
<h3>Child</h3>
<p>last name: {name.lastName}</p>
<p>first name: {name.firstName}</p>
</div>
);
};
export default memo(Child);
예시1 과 같은 코드에서 prop을 객체로 변환하고 버튼 하나를 뺀 코드이다.
자식 컴포넌트도 React.memo로 최적화가 되어있고, props로 전달되는 값은 변하지 않는다. 따라서 자식 컴포넌트는 이미 최적화가 되어있기 때문에 렌더링이 되지 않을것 같다.
하지만 위와같이 자식 컴포넌트 또한 렌더링이 된다.
그 이유는 위의 useMemo 에서도 살펴봤듯이 객체는 원시 타입이 아니라 참조 타입이기 때문이다.
변수에는 값이 저장되는 것이 아닌 메모리의 주소가 저장되는데, 렌더링이 될 때마다 변수가 초기화 되면서 새로운 메모리에 할당이 되므로 결국 새로운 값이라고 인식을 하게 될것이고 Child에 넘겨주는 props 또한 이전과 다른 props라고 인식하게 되므로 Child 또한 결국엔 props check을 통해 다른 값이므로 re-render가 되는것이다.
따라서 이러한 현상을 막기위해 아래와 같이 useMemo도 같이 사용하면 훨씬 최적화 된 컴포넌트를 만들 수 있다.
import React, { useMemo, useState } from 'react';
import Child from './Child';
function App() {
const [parentAge, setParentAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
console.log('Parent Component is rendered');
const name = useMemo(() => {
return {
lastName: '홍',
firstName: '길동',
};
}, []);
return (
<div style={{ border: '2px solid navy', padding: '10px' }}>
<h1>Parent</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>Increment Parent Age</button>
<Child name={name} />
</div>
);
}
export default App;
위와 같이 더 이상 자식 컴포넌트가 새로 랜더링되지 않는다.
예시3 (React.memo + useCallback)
import React, { useState } from 'react';
import Child from './Child';
function App() {
const [parentAge, setParentAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
console.log('Parent Component is rendered');
const tellMe = () => {
console.log('Hello 길동');
}
return (
<div style={{ border: '2px solid navy', padding: '10px' }}>
<h1>Parent</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>Increment Parent Age</button>
<Child name={"홍길동"} tellMe={tellMe} />
</div>
);
}
export default App;
import React, { memo } from 'react';
const Child = ({name, tellMe}) => {
console.log('Child Component is rendered');
return (
<div style={{ border: '2px solid powderblue', padding: '10px' }}>
<h3>Child</h3>
<p>name: {name}</p>
<button onClick={tellMe}>Hello Mom?</button>
</div>
);
};
export default memo(Child);
자식 컴포넌트에서 버튼을 누르면 부모 컴포넌트에서 prop으로 준 함수가 실행되어 홍길동에게 인사를 하는 문구가 콘솔에 찍히도록 만들었다.
prop으로 전달되는 변수와 함수 모두 변하는것이 없지만 부모 컴포넌트가 렌더링이 될 때마다 자식 컴포넌트도 렌더링이 된다. 이는 위와 같은 이유이다.
변수에 함수'객체'로써 값이 아닌 메모리 주소가 저장이 되기 때문에 컴포넌트가 렌더링이 될 마다 새로운 메모리 주소를 할당받기 때문에 다르게 인식하기 때문이다.
따라서 useCallback을 사용해 아래와 같이 최적화를 시켜주면 된다.
import React, { useCallback, useState } from 'react';
import Child from './Child';
function App() {
const [parentAge, setParentAge] = useState(0);
const incrementParentAge = () => {
setParentAge(parentAge + 1);
};
console.log('Parent Component is rendered');
const tellMe = useCallback(() => {
console.log('Hello 길동');
}, []);
return (
<div style={{ border: '2px solid navy', padding: '10px' }}>
<h1>Parent</h1>
<p>age: {parentAge}</p>
<button onClick={incrementParentAge}>Increment Parent Age</button>
<Child name={'홍길동'} tellMe={tellMe} />
</div>
);
}
export default App;
9. Custom Hook
import { useState } from "react";
function App() {
const [inputValue, setInputValue] = useState('');
const handleChange = (e) => {
setInputValue(e.target.value);
};
const handleSubmit = () => {
alert(inputValue);
setInputValue('');
};
return (
<div>
<h1>useInput</h1>
<input value={inputValue} onChange={handleChange} />
<button onClick={handleSubmit}>확인</button>
</div>
)
}
export default App;
위와 같이 간단한 input과 button이 있는 컴포넌트가 있다. 우리는 입력값이 변할때마다 state에 저장하기 위해 따로 함수를 만들고, 버튼을 눌렀을때 alert 창을 띄우기 위해 함수를 또 따로 만들었다.
지금은 input이 하나지만, 여러개 있을경우에는 state를 3개 만들고 또 그 state에 따른 똑같은 형식의 함수들을 더 만들어야 한다. 이것을 custom hook으로 해결할 수 있다.
예시1 ( useInput )
import React, { useState } from 'react';
export function useInput(initialValue, submitAction) {
const [inputValue, setInputValue] = useState(initialValue);
const handleChange = (e) => {
setInputValue(e.target.value);
};
const handleSubmit = () => {
setInputValue('');
submitAction(inputValue);
}
return [inputValue, handleChange, handleSubmit];
}
import { useInput } from './useInput';
function displayMessage(message) {
alert(message);
}
function App() {
const [inputValue, handleChange, handleSubmit] = useInput(
'',
displayMessage
);
return (
<div>
<h1>useInput</h1>
<input value={inputValue} onChange={handleChange} />
<button onClick={handleSubmit}>확인</button>
</div>
);
}
export default App;
위 코드에는 input이 하나밖에 없어서 장점을 느끼지 못할지도 모르지만, 여러개의 input을 가지고 있는경우에 그 진가가 발휘된다. 만약 input이 여러개 있을경우 위에서도 언급했듯이 각 input마다 State를 만들고 또 그에대한 State가 변경될때마다 setState를 해주는 함수를 따로따로 만들어야한다. 하지만 Custom Hook으로 사용할경우에는 값들을 매개변수로 받아서 공통된 형식을 Custom으로 만들어 코드의 양을 줄이고, 재사용성을 높일 수 있다.
Custom Hook의 재사용성이 뛰어난 이유는 Custom Hook이 가지고 있는 State와 Effect는 이것을 사용하는 컴포넌트마다 독립적이다. 또한 한 컴포넌트에 여러번 사용해도 독립적으로 State와 Effect가 여러개 생성이 된다. 따라서 여러개의 input을 처리하기 위해 Custom Hook을 여러개 사용해도 각각의 input은 독립적인 State를 가질 수 있다.
예시2 ( useFetch )
import { useEffect, useState } from 'react';
const baseUrl = 'https://jsonplaceholder.typicode.com';
function App() {
const [data, setData] = useState(null);
const fetchUrl = (type) => {
fetch(baseUrl + '/' + type)
.then((res) => res.json())
.then((res) => setData(res));
};
useEffect(() => {
fetchUrl('users');
}, []);
console.log(data);
return (
<div>
<h1>useFetch</h1>
<button onClick={() => fetchUrl('users')}>Users</button>
<button onClick={() => fetchUrl('posts')}>Posts</button>
<button onClick={() => fetchUrl('todos')}>Todos</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default App;
외부 API로부터 정보를 받아와서 버튼에 따른 형식의 데이터들을 화면에 보여주는 컴포넌트이다.
하지만 useState, fetch, useEffect를 사용하는 저 일련의 과정이 다른 컴포넌트에서도 필요하다면 그때마다 복사 붙여넣기 해서 사용할 것이 아닌 아래와 같이 Custom Hook으로 만들어서 관리하는 것이 좋다.
import React, { useState, useEffect } from 'react';
export function useFetch(baseUrl, initialType) {
const [data, setData] = useState(null);
const fetchUrl = (type) => {
fetch(baseUrl + '/' + type)
.then((res) => res.json())
.then((res) => setData(res));
};
useEffect(() => {
fetchUrl(initialType);
}, []);
return {
data,
fetchUrl,
};
}
import { useFetch } from './useFetch';
const baseUrl = 'https://jsonplaceholder.typicode.com';
function App() {
const { data: userData } = useFetch(baseUrl, 'users');
const { data: postData } = useFetch(baseUrl, 'posts');
return (
<div>
<h1>User</h1>
{userData && <pre>{JSON.stringify(userData[0], null, 2)}</pre>}
<h1>Post</h1>
{postData && <pre>{JSON.stringify(postData[0], null, 2)}</pre>}
</div>
);
}
export default App;
위와 같이 Custom Hook으로 따로 빼니까 재사용성 부분 뿐만 아니라 컴포넌트 내의 구조도 훨씬 깔끔해진것을 볼 수 있다. 그리고 만약 다른 컴포넌트에서 다른 API를 이용해서 데이터를 불러와야 한다면 다른 컴포넌트에서 useFetch에 새로운 baseUrl을 넣어주고 똑같은 형태로 데이터를 불러올 수 있다.
'Web > React' 카테고리의 다른 글
CRA(Create React App) vs. Vite + React (1) | 2024.01.23 |
---|---|
Bundler (0) | 2023.08.16 |
React & React-Dom (0) | 2022.11.11 |
Why React? (0) | 2022.10.11 |