2025.04.09, 04.14
함수 컴포넌트에서 사용 불가능한 생명주기 메소드의 한계점으로 인해 상태 관리 및 렌더링 이후 시점 컨트롤 등 다양한 문제를 해결하기 위해 만든 함수 집합을 의미
컴포넌트가 렌더링 된 이후에 특정 작업을 수행할 내용이 필요한 경우 사용할 수 있는 기능
<div id="root"></div>
<script type="text/babel">
const {useEffect} = React;
function MessagePrinter({message}) {
console.log('렌더링...');
/*
return 이후 실행
비동기 작업(API 호출하는 구문을 실행)
*/
useEffect(() => {
console.log('렌더링 이후 동작...');
console.log(message);
});
return (
<h1>{console.log('렌더링 시 출력')}{message}</h1>
);
/*
return 이후의 코드(렌더링 이후 시점)에서는 일반 코드가 실행되지 않기 때문에,
렌더링 후 작업은 useEffect에서 처리해야 한다
*/
// 이 코드는 실행되지 않음. return 이후이기 때문에 무시됨
// console.log('렌더링 이후 동작...');
}
const message = "안녕하세요";
ReactDOM.createRoot(document.getElementById('root')).render(<MessagePrinter message = {message}/>);
</script>
useEffect는 기본적으로 마운트 된 시점과 업데이트 된 시점 두 가지 모두 동작
<div id="root"></div>
<script type="text/babel">
const {useState, useEffect} = React;
function TimePrinter() {
console.log('랜더링 됨!!')
const [time, setTime] = useState(new Date().toLocaleTimeString());
// useEffect 의 기본구조는 useEffect( () => {실행할 함수} , [의존성 배열] );
useEffect(
() => console.log('마운트 시점에만 동작함...'),
[] // 두 번째 인자로 빈 배열을 넣으면 업데이트 시점에는 더 이상 동작하지 않고 마운트 시점(렌더링 이후)에만 동작하게 된다.
// 실시간 검색 같이 상태값 변화에 따라 동작이 필요한 경우에는 의존성 배열을 비워두면 안된다.
// 서버에 지속적인 연결이 필요한 경우 두번째 인자를 비워두면 안된다.
)
return(
<>
<button onClick={() => setTime(new Date().toLocaleTimeString())}>
현재시간 확인하기
</button>
<h1>{time}</h1>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<TimePrinter/>);
</script>
useEffect는 기본적으로 마운트 시점에도 실행되지만, 특정 값의 변경 시점에만 실행되도록 제어할 수 있다
<div id="root"></div>
<script type="text/babel">
const {useState, useEffect} = React;
function LoginForm() {
const [user, setUser] = useState({
username: '',
password: ''
});
const onChangeHandler = (e) => {
setUser({
...user,
[e.target.name]: e.target.value
});
};
/*
useEffect는 기본적으로 마운트 시점에 동작하고 또한 원하는 값의 변경 시점에만 동작하게 각각 만들 수 있다.
그 뿐 아니라 변화 감지 대상에 변화가 없으면 쓸데없이 동작하지 않는다.
*/
useEffect(
() => {console.log('username update...')},
[user.username] // 동작하기 전의 변경 전 값과 변경 후 값을 비교하여 일치하면 호출을 건너뛴다.
);
useEffect(
() => {console.log('password update...')},
[user.password]
);
return (
<>
<label>username: </label>
<input type="text" name="username" onChange={onChangeHandler}/>
<br/>
<label>password: </label>
<input type="password" name="password" onChange={onChangeHandler}/>
<h3>username: {user.username}</h3>
<h3>password: {user.password}</h3>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<LoginForm/>);
</script>
useEffect(() => { return () => { ... } }, [의존성 배열])
<div id="root"></div>
<script type="text/babel">
const {useState, useEffect} = React;
function Timer() {
useEffect(
() => {
// console.log('Timer 컴포넌트 마운트 될 때만 동작함...');
console.log('타이머가 시작합니다...');
const timer = setInterval(() => {
console.log(new Date().toLocaleTimeString());
},1000);
/* 함수를 반환하면 해당 컴포넌트를 언마운트 시 setInteval을 통해 Interval을 먼저 종료 시킨다. */
return () => {
clearInterval(timer);
console.log('타이머가 종료됩니다...');
}
},
[]
)
return <h1>타이머를 시작합니다.</h1>;
}
function App() {
const [isTrue, setIsTrue] = useState(false);
return(
<>
<button onClick={() => {setIsTrue(!isTrue)}}>타이머 토글</button>
{ isTrue && <Timer/>}
{/* { isTrue ? <Timer/> : null} */}
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
컴포넌트의 상태 업데이트를 외부에 선언한 reducer 함수에 위임하여, 액션(action)에 따라 상태를 관리하는 방식
<div id="root"></div>
<script type="text/babel">
/* useReducer(외부 함수(리듀서)에서 컴포넌트의 state를 action에 따라 관리) */
const {useReducer} = React;
// state : {value: 0} / action : {type: 'INCREMENT or DECREMENT'}
function reducer(state, action) {
console.log(state);
console.log(action);
switch(action.type) {
case 'DECREMENT':
return {value: state.value - 1};
case 'INCREMENT':
return {value: state.value + 1};
default:
return state;
}
}
/* 컴포넌트에서 직접 state를 수정(setState개념)을 하지 않는다. */
function Counter() {
console.log(useReducer(reducer, {value: 0}));
// useReducer 의 기본 틀 useReducer(외부 리듀서 함수 , 초기값)
// dispatch -> reducer / state -> {value: 0}
// dispatch는 reducer 함수를 쫓아다닌다.
const [state, dispatch] = useReducer(reducer, {value: 0});
return (
<>
<h1>counter : {state.value}</h1>
<button onClick={() => dispatch({type: 'DECREMENT'})}>- 1</button>
<button onClick={() => dispatch({type: 'INCREMENT'})}>+ 1</button>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<Counter/>);
</script>
리듀서를 활용하면 input 태그들의 상태 관리를 하나의 state 객체로 간결하게 관리할 수 있다.
<div id="root"></div>
<script type="text/babel">
const {useReducer} = React;
function reducer(state, action) {
return {
...state,
[action.name]: action.value
};
}
function RegistForm() {
const [state, dispatch] = useReducer(reducer, {
name: '',
nickname: ''
});
const {name, nickname} = state;
const onChangeHandler = e => {
console.log(e.target);
dispatch(e.target)};
return (
<>
<label>이름: </label>
<input type="text" name="name" onChange={onChangeHandler}/>
<br/>
<label>닉네임: </label>
<input type="text" name="nickname" onChange={onChangeHandler}/>
<br/>
<div>
<h3>입력한 이름: {name}</h3>
<h3>입력한 닉네임: {nickname}</h3>
</div>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<RegistForm/>);
</script>
무거운 연산 결과를 메모이제이션(캐싱)하여, 불필요한 계산을 방지할 수 있도록 해주는 React Hook
<div id="root"></div>
<script type="text/babel">
/* useMemo 훅스도 꺼내자. */
const {useState, useMemo} = React;
const hardCalculator = (num) => {
console.log('어려운 계산');
for(let i = 0; i < 1999999999; i++) {
//blank
}
return num + 10000;
}
const easyCalculator = (num) => {
console.log('쉬운 계산');
return num + 1;
}
function App() {
const [hardNumber, setHardNumber] = useState(1);
const [easyNumber, setEasyNumber] = useState(1);
// useMemo( () => {실행시킬 함수} , [의존성 배열] )
// hardNumber가 바뀔 때만 재계산
const hardSum = useMemo(() => {
return hardCalculator(hardNumber);
}, [hardNumber]);
// easyNumber가 바뀔 때만 재계산
const easySum = useMemo(() => {
return easyCalculator(easyNumber);
}, [easyNumber]);
return (
<>
<h3>어려운 계산기</h3>
<input
type="number"
value={hardNumber}
onChange={e=> setHardNumber(parseInt(e.target.value))}
/>
<span> + 10000 = {hardSum}</span>
<h3>쉬운 계산기</h3>
<input
type="number"
value={easyNumber}
onChange={e=> setEasyNumber(parseInt(e.target.value))}
/>
<span> + 1 = {easySum}</span>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
useMemo는 참조형 데이터(객체, 배열 등)를 지역 변수로 사용할 때 특히 유용
<div id="root"></div>
<script type="text/babel">
const {useState, useEffect, useMemo} = React;
function App() {
const [isKorea, setIsKorea] = useState(true);
const [number, setNumber] = useState(0);
console.log('렌더링');
/* 1. 지역 변수에 문자열로 초기화(number state 변화 시 location에 변화가 없음(재할당)-기본자료형이므로) */
const location = isKorea? '한국':'외국';
/* useEffect를 사용해서 콜백함수를 mount 시점 및 location값에 변화가 있을 때만 동작 */
useEffect(() => {
console.log('useEffect 호출됨...');
}, [location]);
return (
<>
<h2>지금 당신이 있는 위치는?</h2>
<p>국가: {location}</p>
<button onClick={() => setIsKorea(!isKorea)}>국가 토글 하기</button>
<hr/>
<h2>좋아하는 숫자를 입력해 주세요</h2>
<input
type="number"
value={number}
onChange={e => setNumber(e.target.value)}
/>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
<div id="root"></div>
<script type="text/babel">
const {useState, useEffect, useMemo} = React;
function App() {
const [isKorea, setIsKorea] = useState(true);
const [number, setNumber] = useState(0);
console.log('렌더링');
/* 2. 지역 변수에 객체로 초기화(number state 변화 시 location에 변화가 있음(재할당)-객체의 주소값이므로) */
const location = {
country: isKorea? '한국':'외국'
};
/* useEffect를 사용해서 콜백함수를 mount 시점 및 location값에 변화가 있을 때만 동작 */
useEffect(() => {
console.log('useEffect 호출됨...');
}, [location]);
return (
<>
<h2>지금 당신이 있는 위치는?</h2>
<p>국가: {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>국가 토글 하기</button>
<hr/>
<h2>좋아하는 숫자를 입력해 주세요</h2>
<input
type="number"
value={number}
onChange={e => setNumber(e.target.value)}
/>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
<div id="root"></div>
<script type="text/babel">
const {useState, useEffect, useMemo} = React;
function App() {
const [isKorea, setIsKorea] = useState(true);
const [number, setNumber] = useState(0);
console.log('렌더링');
/* 3. 지역 변수에 useMemo의 반환값으로 초기화(number state 변화 시 location에 변화가 없음(재할당 X)-isKorea가 동일하므로) */
const location = useMemo(() => {
return {
country: isKorea? '한국': '외국'
}
}, [isKorea]);
/* useEffect를 사용해서 콜백함수를 mount 시점 및 location값에 변화가 있을 때만 동작 */
useEffect(() => {
console.log('useEffect 호출됨...');
}, [location]);
return (
<>
<h2>지금 당신이 있는 위치는?</h2>
<p>국가: {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>국가 토글 하기</button>
<hr/>
<h2>좋아하는 숫자를 입력해 주세요</h2>
<input
type="number"
value={number}
onChange={e => setNumber(e.target.value)}
/>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
useCallback은 함수형 컴포넌트 내부에서 선언된 함수를 메모이제이션(memoization)하여, 의존성이 변경되지 않는 한 같은 함수 객체를 재사용하게 만들어주는 Hook
사용 시점
<div id="root"></div>
<script type="text/babel">
const {useState, useEffect} = React;
function App() {
const [number, setNumber] = useState(0);
const [toggle, setToggle] = useState(false);
const printNumber = () => {
console.log('current number: ', number); // console.log는 ,로 구분해서 로그들을 찍어낼 수 있다.
}
/*
number 또는 toggle state가 변경될 때 함수 생성이 다시 되는 것은 불필요하다.
하지만 매번 함수가 다시 생성되어 반환되고 useEffect에서는 해당 지역 변수에 변화가 있다고 인지하게 된다.
*/
useEffect(() => {
console.log('printNumber 값 변화 인지됨');
},[printNumber]);
return (
<>
<input
type="number"
value={number}
onChange={e => setNumber(e.target.value)}
/>
<button onClick={() => setToggle(!toggle)}>{String(toggle)}</button> {/* boolean값은 문자열로 바꿔야 JSX문법으로 화면에 표현된다. */}
<br/>
<button onClick={printNumber}>PrintNumberState</button>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
<div id="root"></div>
<script type="text/babel">
const {useState, useEffect, useCallback} = React;
function App() {
const [number, setNumber] = useState(0);
const [toggle, setToggle] = useState(false);
/*
함수 생성이 불필요하게 계속 될 때 useCallback을 이용하여
함수를 memorization해서 사용할 수 있다.
*/
const printNumber = useCallback(
() => {
console.log('current number: ', number);
},
[number] // 의존성 배열 자리에 빈 배열을 두게 되면 마운트 시점에 한번 지역변수 초기화를 위해 사용되고 나서 함수는
// 새로 정의 되지 않으므로 항상 number의 초기값인 0만 나오게 된다.
);
useEffect(() => {
console.log('printNumber 값 변화 인지됨');
},[printNumber]);
return (
<>
<input
type="number"
value={number}
onChange={e => setNumber(e.target.value)}
/>
<button onClick={() => setToggle(!toggle)}>{String(toggle)}</button>
<br/>
<button onClick={printNumber}>PrintNumberState</button>
</>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
context는 React 컴포넌트 트리 안에서 전역적으로 데이터를 공유할 수 있도록 고안된 방법
<div id="root"></div>
<script type="text/babel">
const {useState} = React;
function Header({isDark}) {
return (
<header
className="header"
style={
{
backgroundColor: isDark? 'black': 'lightgray',
color: isDark? 'white': 'black'
}
}
>
<h1>Welcome to Greedy World!</h1>
</header>
);
}
function Content({isDark}) {
return (
<div
className="content"
style={
{
backgroundColor: isDark? 'gray': 'white',
color: isDark? 'white': 'black'
}
}
>
<p>내용입니다.</p>
</div>
);
}
function Footer({isDark, setIsDark}) {
const toggleHandler = () => {setIsDark(!isDark)}
return (
<footer
className="footer"
style={
{
backgroundColor: isDark? 'black': 'lightgray',
color: isDark? 'white': 'black'
}
}
>
<button onClick={toggleHandler}>{isDark? 'Light Mode': 'Dark Mode'}</button>
Copyright 2025. ohgiraffers. all rights reserved.
</footer>
);
}
function Page({isDark, setIsDark}) {
return (
<div className="page">
<Header isDark={isDark}/>
<Content isDark={isDark}/>
<Footer isDark={isDark} setIsDark={setIsDark}/>
</div>
);
}
function App() {
const [isDark, setIsDark] = useState(false);
return <Page isDark={isDark} setIsDark={setIsDark}/>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
<div id="root"></div>
<script type="text/babel">
/* createContext, useContext 추가 */
const {useState, createContext, useContext} = React;
/*
context 객체를 createContext를 통해 만들게 되고 defaultValue가 없는 경우에는 null로 설정할 수 있다.
context 객체를 구독하고 있는 컴포넌트를 렌더링 할 때 React는 트리 상위에서 가장 가까이 있는
Provider로부터 현재 값을 읽어들인다. 이 때 적절한 provider를 찾지 못할 때 쓰이는 값이 defaultValue이다.
*/
const DarkModeContext = createContext(null);
function Header() {
const context = useContext(DarkModeContext);
const {isDark} = context;
return (
<header
className="header"
style={
{
backgroundColor: isDark? 'black': 'lightgray',
color: isDark? 'white': 'black'
}
}
>
<h1>Welcome to Greedy World!</h1>
</header>
);
}
function Content() {
const context = useContext(DarkModeContext);
const {isDark} = context;
return (
<div
className="content"
style={
{
backgroundColor: isDark? 'gray': 'white',
color: isDark? 'white': 'black'
}
}
>
<p>내용입니다.</p>
</div>
);
}
function Footer() {
const context = useContext(DarkModeContext);
// console.log(context);
const {isDark, setIsDark} = context;
const toggleHandler = () => {setIsDark(!isDark)}
return (
<footer
className="footer"
style={
{
backgroundColor: isDark? 'black': 'lightgray',
color: isDark? 'white': 'black'
}
}
>
<button onClick={toggleHandler}>{isDark? 'Light Mode': 'Dark Mode'}</button>
Copyright 2025. ohgiraffers. all rights reserved.
</footer>
);
}
function Page() {
return (
<div className="page">
<Header/>
<Content/>
<Footer/>
</div>
);
}
function App() {
const [isDark, setIsDark] = useState(false);
/*
Provider는 value prop을 이용하여 하위 컴포넌트에게 값을 전달한다.
이 때 값을 전달받을 수 있는 컴포넌트 수에는 제한이 없다.
해당 Provider는의 Context 이름으로부터 useContext hooks를 활용하여
어느 컴포넌트든 필요한 순간 가져다 쓸 수 있게 한다.
*/
return (
<DarkModeContext.Provider value={{isDark, setIsDark}}>
<Page/>
</DarkModeContext.Provider>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
반복되는 코드를 줄이기 위해(개선하기 위해) 사용하는 사용자 정의 훅스
<div id="root"></div>
<script type="text/babel">
const {useState} = React;
/* useInput 커스텀 훅*/
function useInput() {
/*
input 태그에 입력 받은 값을 state로 관리하는 이유
1. 사용자가 입력한 값을 선택자 없이 읽어오기 위함
2. react는 form태그를 쓰지 않는다.(state의 값을 비동기 방식 통신을 하여 다른 곳으로 전송하게 된다.)
*/
const [value, setValue] = useState('');
const onChange = (e) => setValue(e.target.value);
return {value, onChange};
}
function App() {
const name = useInput();
const password = useInput();
const email = useInput();
return (
<div>
<label>이름: </label>
<input type="text" value={name.value} onChange={name.onChange}/>
<br/>
<label>비밀번호: </label>
<input type="password" {...password}/> {/* 바벨에서 제공되는 축약형 문법 */}
<br/>
<label>이메일: </label>
<input type="email" {...email}/>
<br/>
<h4>name: {name.value}</h4>
<h4>password: {password.value}</h4>
<h4>email: {email.value}</h4>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>