
7일차 오후 수업엔 react의 훅 함수에 대해 학습했습니다. react가 vanila JS에 비해 편하다라고 느껴지려면 어느 정도 경지에 올라야 할 것 같습니다. ㅠㅠ react에 대해 눈에 익기도 전에 프론트엔드 수업이 거의 끝나간다는 게 믿기지 않지만, 빠른 일정 속에서 오늘 배운 내용을 정리해보았습니다.
클래스형 컴포넌트에서 제공하는 기능을 함수형 컴포넌트에서 사용할 수 있도록 도입된 함수.
(state, ref, lifecycle method, ... 등) . 훅 함수의 이름은 use 접두어를 사용한다.
import { useState } from "react";
// 사용자 이름과 별명을 관리
function Info() {
const [name, setName] = useState("");
const [nickname, setNickname] = useState("");
const changeName = e => setName(e.target.value);
const changeNickname = e => setNickname(e.target.value);
return (
<>
<div>
<p>이름: {name}</p>
<p>별명: {nickname}</p>
</div>
<div>
<p>이름: <input type="text" name="name" value={name} onChange={changeName} /></p>
<p>별명: <input type="text" name="nickname" value={nickname} onChange={changeNickname} /></p>
</div >
</>
);
}
export default Info;
import Info from "./Info";
const App = () => {
return <Info />;
};
export default App;
다른 방식의 코드
import Info from "./Info";
export default () => <Info />;
클래스형 컴포넌트의 componentDidMount, componentDidUpdate, comdponentWillUnmount를 합친 형태.

순서 : 마운트(ComponentDidMount) -> name 업데이트(ComponentDidUpdate) -> nickname 업데이트

// 의존성 배열을 생략 => 마운트, 업데이트 모두 이펙트 함수를 실행
useEffect(() => {
console.log("렌더링이 완료되었습니다.");
console.log({ name, nickname });
});
useEffect(() => {
console.log("렌더링이 완료되었습니다.");
console.log({ name, nickname });
}, []);

// 마운트될 때와 name 상태변수의 값이 변경될 때 이펙트 함수를 실행
useEffect(() => {
console.log("렌더링이 완료되었습니다.");
console.log({ name, nickname });
}, [name]);
컴포넌트가 언마운트 또는 리렌더링되기 전에 어떤 작업을 수행하고 싶은 경우, 이펙트 함수에서 후처리 작업을 수행하는 함수를 반환
useEffect(() => {
console.log("렌더링이 완료되었습니다.");
console.log({ name, nickname });
return () => console.log("cleanup", name); ⇐ name 상태변수가 변경되거나 컴포넌트가 언마운트될 때 실행
}, [name]);
[App.js] 수정
import { useState } from "react";
import Info from "./Info";
export default () => {
const [isVisible, setIsVisible] = useState(false);
const changeIsVisible = () => setIsVisible(!isVisible);
return (
<>
<button onClick={changeIsVisible}>{isVisible ? "숨기기" : "보이기"}</button>
<br />
{isVisible && <Info />}
</>
);
};
useEffect(() => {
console.log("렌더링이 완료되었습니다.");
console.log({ name, nickname });
return () => console.log("cleanup", name); ⇐ 컴포넌트가 언마운트될 때만 호출출
}, []);
함수형 컴포넌트에서 DOM 요소를 직접 제어하기 위해서 사용한다. ref 속성(attribute)와 useRef 훅을 사용해 HTML DOM 노드와 JavaScript 코드를 연결한다.
useRef 훅은 current 프로퍼티를 가지는 객체를 반환한다.


import Average from "./Average"
export default () => {
return <Average />;
};
import React, { useRef, useState } from "react";
export default function Average() {
const [number, setNumber] = useState("");
const [list, setList] = useState([]);
const changeNumber = e => setNumber(e.target.value);
const changeList = () => {
const newList = list.concat(number); // [...list, number]
setList(newList);
setNumber("");
** refNumber.current.focus();**
};
** // const refNumber = React.createRef();
const refNumber = useRef();**
return (
<>
<div>
** <input ref={refNumber} type="number" value={number} onChange={changeNumber} />**
<button onClick={changeList}>등록</button>
</div>
<div>
<p>입력값: {number}</p>
</div>
<div>
등록된 숫자
<ul>
{
list.map((n, i) => <li key={i}>{n}</li>)
}
</ul>
</div>
</>
);
}
화면에서 특정 element 직접 제어할 때, 타이머, 렌더링과 무관하게 값을 유지해야 할때 : 상태변수나 ref 변수로 조작. local 변수는 X.
[App.js ]
import "./App.css";
import { useState, useRef } from "react";
const CounterWithLocalVariable = () => {
const [count, setCount] = useState(0);
let intervalId = 0;
console.log(`렌더링... count: ${count}, intervalId: ${intervalId}`);
const startCounter = () => {
intervalId = setInterval(() => { setCount(count => count + 1) }, 1000);
console.log(`카운터 시작... intervalId: ${intervalId}`);
};
const stopCounter = () => {
clearInterval(intervalId);
console.log(`카운터 정지... intervalId: ${intervalId}`);
};
return (
<>
<p>카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
};
const CounterWithStateVariable = () => {
const [count, setCount] = useState(0);
const [intervalId, setInterId] = useState(0);
console.log(`렌더링... count: ${count}, intervalId: ${intervalId}`);
const startCounter = () => {
const id = setInterval(() => { setCount(count => count + 1) }, 1000);
setInterId(id);
console.log(`카운터 시작... intervalId: ${intervalId}`);
};
const stopCounter = () => {
clearInterval(intervalId);
console.log(`카운터 정지... intervalId: ${intervalId}`);
};
return (
<>
<p>카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
};
const CounterWithRefVariable = () => {
const [count, setCount] = useState(0);
const intervalId = useRef(0);
console.log(`렌더링... count: ${count}, intervalId: ${intervalId.current}`);
const startCounter = () => {
intervalId.current = setInterval(() => { setCount(count => count + 1) }, 1000);
console.log(`카운터 시작... intervalId: ${intervalId.current}`);
};
const stopCounter = () => {
clearInterval(intervalId.current);
console.log(`카운터 정지... intervalId: ${intervalId.current}`);
};
return (
<>
<p>카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
};
export default () => {
return (
<>
<CounterWithLocalVariable />
<CounterWithStateVariable />
<CounterWithRefVariable />
</>
);
};
상태 관리 로직이 복잡할 때 useState 대체품으로 사용.
현재 상태와 액션을 받아 새로운 상태를 반환하는 리듀서(reducer) 함수를 통해 상태를 관리.
useReducer는 리듀스 함수와 초기 상태를 인자로 받아 상태와 디스패치 함수를 반환.
- 리듀스 함수 : 현재 상태와 액션을 받아 새로운 상태를 반환하는 순수함수
- 초기 상태 : 상태의 초기값
- 액션 : 상태를 변경하기 위한 정보가 담긴 객체로, 일반적으로 type 프로퍼티를 가지고 있으며, 필요에 따라 데이터를 포함할 수 있음
- 디스패치 함수 : 액션을 리듀서로 전달해 상태를 업데이트하는 함수
사용 예시)
- 상태 로직이 복잡하거나, 여러 하위 값으로 구성된 상태를 관리해야 할 때
- 상태 업데이트 로직이 여러 종류의 액션에 의해 다르게 동작해야 할 때
- 상태와 그 업데이트 로직을 컴포넌트에서 분리하고 싶을 때
[App.js]
import "./App.css";
import { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const changeCount = e => setCount(count + Number(e.target.innerText));
⇐ 버튼의 내용(contents)이 숫자로 변환 가능하기 때문에 하나의 핸들러 함수로 구현이 가능
return (
<>
<div>현재 카운터 값은 <b>{count}</b>입니다.</div>
<div>
<button onClick={changeCount}>+1</button>
<button onClick={changeCount}>-1</button>
</div>
</>
);
};
export default () => <Counter />;
import "./App.css";
import { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const changeCountPlus = () => setCount(count + 1);
const changeCountMinus = () => setCount(count - 1);
return (
<>
<div>현재 카운터 값은 <b>{count}</b>입니다.</div>
<div>
<button onClick={changeCountPlus}>하나 더하기</button>
<button onClick={changeCountMinus}>하나 빼기</button>
</div>
</>
);
};
export default () => <Counter />;
import "./App.css";
import { useReducer, useState } from "react";
// state: 현재 상태 변수의 값
// action: 상태 변수 변경에 필요한 조건과 값 (호출 시 전달되는 값)
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
const Counter = () => {
/*
const [count, setCount] = useState(0);
const changeCountPlus = () => setCount(count + 1);
const changeCountMinus = () => setCount(count - 1);
*/
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<div>현재 카운터 값은 <b>{state.count}</b>입니다.</div>
<div>
<button onClick={() => dispatch({ type: "INCREMENT" })}>하나 더하기</button>
<button onClick={() => dispatch({ type: "DECREMENT" })}>하나 빼기</button>
</div >
</>
);
};
export default () => <Counter />;
import "./App.css";
import { useState } from "react";
const Info = () => {
const [name, setName] = useState('');
const [nickName, setNickName] = useState('');
const changeName = e => setName(e.target.value);
const changeNickName = e => setNickName(e.target.value);
return (
<>
<div>
<p>이름: {name}</p>
<p>별명: {nickName}</p>
</div>
<div>
<p>이름: <input type="text" value={name} onChange={changeName} /></p>
<p>별명: <input type="text" value={nickName} onChange={changeNickName} /></p>
</div>
</>
);
};
export default () => <Info />;
import "./App.css";
import { useReducer } from "react";
const reducer = (state, action) => {
return { ...state, [action.type]: action.value };
};
const Info = () => {
const [state, dispatch] = useReducer(reducer, { name: "", nickName: "" });
const { name, nickName } = state;
const changeValue = e => dispatch({ type: e.target.name, value: e.target.value });
return (
<>
<div>
<p>이름: {name}</p>
<p>별명: {nickName}</p>
</div>
<div>
<p>이름: <input type="text" name="name" value={name} onChange={changeValue} /></p>
<p>별명: <input type="text" name="nickName" value={nickName} onChange={changeValue} /></p>
</div>
</>
);
};
export default () => <Info />;
성능 최적화를 위해 특정 값이 변경될 때만 메모이제이션된 값을 재계산하도록 하여 불필요한 계산을 방지한다.
계산 비용이 높은 작업이나 렌더링 중에 자주 호출되는 작업에 유용하다.

useMemo로 전달된 함수는 렌더링이 일어나는 동안 실행된다. 일반적으로 렌더링이 일어나는 동안 실행해서는 안될 작업을 useMemo() 함수에 넣으면 안됨.
의존성 배열을 넣지 않을 경우, 렌더링이 일어날 때마다 매번 함수를 실행한다.
import "./App.css";
import Average from "./Average";
export default () => <Average />;
Average 컴포넌트에 저장된 숫자들의 평균을 구해서 출력하는 기능을 추가 ⇒ 숫자를 입력하는 도중에도 불필요하게 평균값을 계산 ⇒ 불필요한 리소스 사용 및 렌더링 지연 문제가 발생.
useMemo 훅을 이용해서 리스트에 숫자가 등록된 경우에만 평균값을 계산하도록 수정.
import React, { useMemo, useRef, useState } from "react";
const getAverage = (numbers) => {
console.log("평균값 계산 중 ...");
// 빈 배열인 경우 0을 반환
if (numbers.length === 0)
return 0;
// 평균을 계산 => 총합을 계산해서 배열의 길이로 나눈 값을 반환
const total = numbers.reduce((prev, curr) => prev + curr);
console.log(total);
return total / numbers.length;
};
export default function Average() {
const [number, setNumber] = useState("");
const [list, setList] = useState([]);
const changeNumber = e => setNumber(e.target.value);
const changeList = () => {
const newList = list.concat(Number(number)); // [...list, number]
setList(newList);
setNumber("");
refNumber.current.focus();
};
const refNumber = useRef();
const avg = useMemo(() => getAverage(list), [list]);
return (
<>
<div>
<input ref={refNumber} type="number" value={number} onChange={changeNumber} />
<button onClick={changeList}>등록</button>
</div>
<div>
<p>입력값: {number}</p>
<p>평균값: {avg}</p>
</div>
<div>
등록된 숫자
<ul>
{
list.map((n, i) => <li key={i}>{n}</li>)
}
</ul>
</div>
</>
);
}
useMemo 훅과 유사하게 성능 최적화를 위해 사용.
useCallback은 콜백 함수가 불필요하게 다시 생성되는 걸 방지.
컴포넌트가 리렌더링될 때 동일한 콜백 함수가 사용되도록 함.
콜백 함수가 자식 컴포넌트의 props로 전달되는 경우 유용.

import { useState, memo } from "react";
import "./App.css";
// React.memo()로 컴포넌트를 래핑하면,
// 리액트를 컴포넌트를 렌더링하고 그 결과를 메모이징(Memoizing)해
// 다음 렌더링이 일어날 때 props가 같으면 메모이징된 내용을 재사용
const Todos = memo(({ todos, addTodo }) => {
console.log(addTodo);
console.log("Child component is rendering...");
return (
<div>
<button onClick={addTodo}>Add Todo</button>
<h2>Todos</h2>
{todos.map((todo, index) => <p key={index}>{todo}</p>)}
</div>
);
});
export default function App() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const increment = () => setCount(count + 1);
// addTodo 함수의 내용이 변경되지 않았음에도 불구하고, <Todos> 컴포넌트를 리렌더링 함
// Todos 컴포넌트에 addTodo props만 남겨두고, todos props를 제거해서 확인해 볼 수 있음
const addTodo = () => {
setTodos([...todos, "할일"]);
};
return (
<>
<Todos todos={todos} addTodo={addTodo} />
<hr />
<div>
<button onClick={increment}>카운트 증가</button>
<h2>Count: {count}</h2>
</div>
</>
);
}
import { useState, memo, useCallback } from "react";
import "./App.css";
const Todos = memo(({ todos, addTodo }) => {
console.log(addTodo);
console.log("Child component is rendering...");
return (
<div>
<button onClick={addTodo}>Add Todo</button>
<h2>Todos</h2>
{todos.map((todo, index) => <p key={index}>{todo}</p>)}
</div>
);
});
export default function App() {
const [count, setCount] = useState(0);
const [todos, setTodos] = useState([]);
const increment = () => setCount(count + 1);
const addTodo = useCallback(() => {
setTodos([...todos, "할일"]);
}, [todos]);
return (
<>
<Todos todos={todos} addTodo={addTodo} />
<hr />
<div>
<button onClick={increment}>카운트 증가</button>
<h2>Count: {count}</h2>
</div>
</>
);
}
함수형 컴포넌트에서 Context API를 쉽게 사용할 수 있도록 도와주는 훅

[ThemedButton, Blog, new 컴포넌트 생성]

import { createContext, useContext, useState } from "react";
import "./App.css";
// #1 Context 생성
const ThemeContext = createContext();
// #2-1 Provider 정의
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
const changeTheme = () => setTheme(theme === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeContext.Provider>
);
}
// const ThemedButton = ({ theme, changeTheme }) => {
const ThemedButton = () => {
// #3 Context 소비
const { theme, changeTheme } = useContext(ThemeContext);
return (
<button onClick={changeTheme}
style={{
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "yellow"
}}>테마 변경</button>
);
};
// const Blog = ({ theme }) => {
const Blog = () => {
// #3 Context 소비
const { theme } = useContext(ThemeContext);
return (
<div style={{
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "#fff"
}}>
<h1>블로그</h1>
<hr />
<h2>블로그 제목</h2>
<p>블로그 내용</p>
</div>
);
};
// const News = ({ theme }) => {
const News = () => {
// #3 Context 소비
const { theme } = useContext(ThemeContext);
return (
<div style={{
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "#fff"
}}>
<h1>뉴스</h1>
<hr />
<h2>뉴스 제목</h2>
<p>뉴스 내용</p>
</div>
);
};
/* 하위 컴포넌트로 전달을 위한 props 변수 생성(정의)하지 않아도 됨
const Contents = ({ theme }) => {
return (
<>
<Blog theme={theme} />
<News theme={theme} />
</>
);
};
*/
const Contents = () => {
return (
<>
<Blog />
<News />
</>
);
};
// 2-2. 컨텍스트 변수를 공유할 컴포넌트를 Provider를 둘러쌈
export default function App() {
/* Provider에서 정의
const [theme, setTheme] = useState("light");
const changeTheme = () => setTheme(theme === "light" ? "dark" : "light");
*/
/* ThemeProvider로 감싸고, props 변수를 삭제 */
return (
<ThemeProvider>
<div>
<h1>테마가 적용된 페이지</h1>
<ThemedButton />
<Contents />
</div>
</ThemeProvider>
);
}
export default function App() {
const [theme, setTheme] = useState("light");
const changeTheme = () => setTheme(theme === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
<div>
<h1>테마가 적용된 페이지</h1>
<ThemedButton />
<Contents />
</div>
</ThemeContext.Provider>
);
}
import { createContext } from "react";
const ThemeContext = createContext();
export default ThemeContext;
import { useState } from "react";
import ThemeContext from "./ThemeContext";
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
const changeTheme = () => setTheme(theme === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeContext.Provider>
);
};
export default ThemeProvider;
import { useContext } from "react";
import ThemeContext from "./ThemeContext";
const ThemedButton = () => {
const { theme, changeTheme } = useContext(ThemeContext);
return (
<button onClick={changeTheme}
style={{
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "yellow"
}}>테마 변경</button>
);
};
import { useContext } from "react";
import ThemeContext from "./ThemeContext";
const Blog = () => {
const { theme } = useContext(ThemeContext);
return (
<div style={{
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "#fff"
}}>
<h1>블로그</h1>
<hr />
<h2>블로그 제목</h2>
<p>블로그 내용</p>
</div>
);
};
export default Blog;
import { useContext } from "react";
import ThemeContext from "./ThemeContext";
const News = () => {
const { theme } = useContext(ThemeContext);
return (
<div style={{
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "#fff"
}}>
<h1>뉴스</h1>
<hr />
<h2>뉴스 제목</h2>
<p>뉴스 내용</p>
</div>
);
};
export default News;
import Blog from "./Blog";
import News from "./News";
const Contents = () => {
return (
<>
<Blog />
<News />
</>
);
};
export default Contents;
import "./App.css";
import Contents from "./Contents";
import ThemedButton from "./ThemedButton";
import ThemeProvider from "./ThemeProvider";
export default function App() {
return (
<ThemeProvider>
<div>
<h1>테마가 적용된 페이지</h1>
<ThemedButton />
<Contents />
</div>
</ThemeProvider>
);
}
import "./App.css";
import { useState } from "react";
import Contents from "./Contents";
import ThemedButton from "./ThemedButton";
// import ThemeProvider from "./ThemeProvider";
import ThemeContext from "./ThemeContext";
export default function App() {
const [theme, setTheme] = useState("light");
const changeTheme = () => setTheme(theme === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
<div>
<h1>테마가 적용된 페이지</h1>
<ThemedButton />
<Contents />
</div>
</ThemeContext.Provider>
);
}
import { useReducer, useState } from "react";
function reducer(state, action) {
return {
...state,
[action.name]: action.value
};
}
function useInputs(initState) {
const [state, dispatch] = useReducer(reducer, initState);
const handlerChange = e => {
dispatch(e.target);
};
return [state, handlerChange];
}
function Info() {
/*
const [state, dispatch] = useReducer(reducer, { name: '', nickname: '' });
const { name, nickname } = state;
const handlerChange = (e) => {
dispatch(e.target);
};
*/
const [state, handlerChange] = useInputs({ name: '', nickname: '' });
const { name, nickname } = state;
return (
<>
<div>
<p>이름: {name}</p>
<p>별명: {nickname}</p>
</div>
<div>
<p>이름: <input type="text" value={name} name="name" onChange={handlerChange} /></p>
<p>별명: <input type="text" value={nickname} name="nickname" onChange={handlerChange} /></p>
</div>
</>
);
}
const App = () => {
return (
<>
<Info />
</>
);
};
export default App;
훅 함수는 용도에 따라 종류도 다양하고, 그 용도를 이해하는 것도 어렵게 느껴집니다. 라이프사이클을 이해하고, 왜 그 시점에 이 함수가 필요한지 알아두어야합니다.