6 Hook
1. Hook
Hook은 함수형 컴포넌트에서 React 기능을 사용할 수 있게 해주는 특별한 함수다.
이름이 항상 use로 시작하는 것이 특징이다.
1.1. Hook 사용 규칙
Hook을 사용할 때 반드시 지켜야 할 두 가지 규칙이 있다.
| 규칙 | 설명 |
|---|---|
| 컴포넌트 최상위에서만 호출 | if문, for문, 중첩 함수 안에서 호출하면 안 된다 |
| React 함수에서만 호출 | 일반 JavaScript 함수에서는 사용할 수 없다 |
1// ❌ 잘못된 예시2function App() {3 if (condition) {4 const [count, setCount] = useState(0); // if문 안에서 호출 → 오류!5 }6}7
8// ✅ 올바른 예시9function App() {10 const [count, setCount] = useState(0); // 최상위에서 호출11}1.2. 자주 쓰는 Hook 목록
| Hook | 용도 |
|---|---|
useState | 상태(데이터) 관리 |
useEffect | 화면 표시/사라짐/업데이트 시 실행 |
useRef | DOM 요소 직접 접근, 렌더링 없이 값 저장 |
useContext | 컴포넌트 트리 전체에 데이터 전달 |
useMemo | 복잡한 계산 결과 캐싱 (성능 최적화) |
useCallback | 함수 캐싱 (성능 최적화) |
이 챕터에서는 가장 많이 쓰는 useState와 useEffect를 집중적으로 다룬다.
2. UseState()
2.1. State란?
State는 컴포넌트의 데이터 저장소다. 값이 변경되면 화면이 자동으로 다시 그려진다.
2.1.1. 기본 사용법
1import { useState } from "react";2
3function App() {4 const [count, setCount] = useState(0);5
6 return (7 <div>8 <p>횟수: {count}</p>9 <button onClick={() => setCount(count + 1)}>증가</button>10 </div>11 );12}2.2. useState 동작 원리
useState는 두 개의 값을 배열로 반환한다.
- 첫 번째: 현재 상태 값
- 두 번째: 상태를 변경하는 함수
1const [value, setValue] = useState(초기값);2.2.1. 예제: 카운터
1function Counter() {2 const [num, setNum] = useState(0);3
4 const plus = () => setNum(num + 1);5 const minus = () => setNum(num - 1);6
7 return (8 <div>9 <h2>{num}</h2>10 <button onClick={plus}>+</button>11 <button onClick={minus}>-</button>12 </div>13 );14}2.3. 객체와 배열 State 관리
주의
주의: 직접 수정 금지!
상태 객체나 배열을 직접 수정하면 안 된다. 반드시 새로운 객체/배열을 만들어야 한다.
전개 연산자 를 사용하면 편리하다.
...은 객체나 배열의 내용물을 꺼내서 펼치는 자바스크립트 문법이다.
2.3.0.0.0.1. 전개연산자를 사용하여 객체를 수정
1const user = { name: "철수", age: 20 };2const newUser = { ...user, name: "영희" };2.3.0.0.0.2. 전개연산자를 사용하지 않고 객체를 수정
1const user = { name: "철수", age: 20 };2const newUser = { name: "영희", age: user.age };2.3.0.0.0.3. 전개연산자를 사용하여 배열을 수정
1const fruits = ["사과", "바나나"];2const newFruits = [...fruits, "오렌지"];2.3.0.0.0.4. 전개연산자를 사용하지 않고 배열을 수정
1const fruits = ["사과", "바나나"];2const newFruits2 = fruits.concat("오렌지");2.3.1. 예제 2: Todo
- Todo.css
- Todo.jsx(추가)
- Todo.jsx(삭제)
1/* 전체 영역 설정 */2.container {3 max-width: 400px;4 margin: 50px auto;5 padding: 20px;6 border: 1px solid #ddd;7 border-radius: 8px;8}9
10/* 입력창과 버튼 가로 배치 */11.input-box {12 display: flex;13 gap: 10px;14 margin-bottom: 20px;15}16
17/* 입력창 크기 확장 */18.input-field {19 flex: 1;20 padding: 10px;21}22
23/* 추가 버튼 디자인 */24.add-btn {25 padding: 10px 20px;26 background-color: #007bff;27 color: white;28 border: none;29 cursor: pointer;30}31
32/* 목록 스타일 초기화 */33.list-box {34 list-style: none;35 padding: 0;36}37
38/* 개별 항목 디자인 */39.list-item {40 padding: 10px;41 border-bottom: 1px solid #eee;42}추가
1import { useState } from "react";2
3function Todo() {4 const [todos, setTodos] = useState([]);5 const [inputValue, setInputValue] = useState("");6
7 const addTodo = () => {8 if (inputValue.trim() === "") return;9 setTodos([...todos, inputValue]);10 setInputValue("");11 };12
13 return (14 <div className="container">15 <div className="input-box">16 <input17 className="input-field"18 value={inputValue}19 onChange={(e) => setInputValue(e.target.value)}20 placeholder="할 일을 입력하세요"21 />22 <button className="add-btn" onClick={addTodo}>추가</button>23 </div>24 <ul className="list-box">25 {todos.map((todo, index) => (26 <li key={index} className="list-item">{todo}</li>27 ))}28 </ul>29 </div>30 );31}- 상태 관리 변수: todos는 할 일 목록 배열을, inputValue는 입력창의 문자열을 관리한다.
- 상태 갱신 함수: addTodo 함수 내부에서 전개 구문(…todos)을 사용하여 기존 배열을 복사한 후 새 항목을 추가한다.
삭제 기능이 포함된 리스트
1const deleteTodo = (index) => {2 setTodos(todos.filter((_, i) => i !== index));3};4
5return (6 <ul>7 {todos.map((todo, index) => (8 <li key={index}>9 {todo} <button onClick={() => deleteTodo(index)}>삭제</button>10 </li>11 ))}12 </ul>13);3. useEffect()
컴포넌트내의 함수가 호출되는 시점을 제어하는 훅
useEffect는 컴포넌트가 화면에 나타날 때, 사라질 때, 또는 특정 값이 변할 때 실행할 코드를 정의한다.
3.1. 문법정리
| 상황 | 예시 |
|---|---|
| 첫 렌더링 시 실행 (마운트) | useEffect(() => { console.log("마운트");}, []) |
| 렌더링될 때마다 실행 | useEffect(() => { console.log("렌더링");}) |
| 특정 값 변경 시 실행 | useEffect(() => { console.log(값);}, [값]) |
| 컴포넌트 제거 시 정리 (언마운트) | useEffect(() => { console.log("마운트"); return () => console.log("언마운트");}, []) |
| 값 변경 시 실행 + 이전 것 정리 | useEffect(() => { console.log(값); return () => console.log("정리");}, [값]) |
3.1.1. 마운트시
컴포넌트가 화면에 처음 나타날 때 단 한 번 실행된다.
빈 배열 []을 의존성 배열로 전달한다.
1useEffect(() => {2 console.log("컴포넌트가 화면에 나타남");3}, []);3.1.2. 렌더링시
의존성 배열을 생략하면 렌더링될 때마다 실행된다. 상태나 props가 바뀔 때마다 매번 호출되므로 주의해서 사용한다.
1useEffect(() => {2 console.log("렌더링됨");3});3.1.3. 특정값 변경시
의존성 배열에 값을 넣으면 그 값이 바뀔 때마다 실행된다.
1const [count, setCount] = useState(0);2
3useEffect(() => {4 console.log("count가 바뀜:", count);5}, [count]);3.1.4. 언마운트시
return으로 **정리 함수(cleanup)**를 반환하면 컴포넌트가 화면에서 사라질 때 실행된다.
타이머, 이벤트 리스너 등 정리가 필요한 경우에 사용한다.
1useEffect(() => {2 console.log("마운트");3
4 return () => {5 console.log("언마운트 - 정리 실행");6 };7}, []);3.2. 실습: 로그인
웹브라우저의 데이터 저장 공간인 로컬 스토리지에 로그인 정보를 저장하는 로그인 페이지를 만들어보자
- App.css
- App.jsx
1.container {2 display: flex;3 flex-direction: column;4 align-items: center;5 min-height: 100vh;6}7
8
9.card {10 background: white;11 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);12 border-radius: 10px;13 padding: 2rem;14 margin: 2rem auto;15 width: 90%;16 max-width: 40rem;17 text-align: center;18}19
20.control {21 margin: 1rem 0;22 display: flex;23 flex-direction: column;24 text-align: left;25}26
27.control label {28 font-weight: bold;29 margin-bottom: 0.5rem;30}31
32.control input {33 font: inherit;34 padding: 0.5rem;35 border-radius: 6px;36 border: 1px solid #ccc;37}38
39.actions {40 text-align: center;41 margin-top: 1rem;42}43
44.btn {45 font: inherit;46 background: #7a0141;47 border: 1px solid #7a0141;48 color: white;49 padding: 0.75rem 2rem;50 border-radius: 30px;51 cursor: pointer;52}53
54.btn:disabled {55 background: #ccc;56 border-color: #ccc;57 color: #666;58 cursor: not-allowed;59}60
61.btn:hover:not(:disabled) {62 background: #a40256;63 border-color: #a40256;64}1import { useState, useEffect } from "react";2import "./App.css";3
4const App = () => {5 const [isLoggedIn, setIsLoggedIn] = useState(false);6 const [email, setEmail] = useState("");7 const [password, setPassword] = useState("");8
9 useEffect(() => {10 const storedLoginInfo = localStorage.getItem("isLoggedIn");11 if (storedLoginInfo === "1") {12 setIsLoggedIn(true);13 }14 }, []);15
16 const loginHandler = (e) => {17 e.preventDefault();18 localStorage.setItem("isLoggedIn", "1");19 setIsLoggedIn(true);20 };21
22 const logoutHandler = () => {23 localStorage.removeItem("isLoggedIn");24 setIsLoggedIn(false);25 setEmail("");26 setPassword("");27 };28
29 return (30 <div className="container">31 <header className="main-header">32 <h1>React Auth Demo</h1>33 {isLoggedIn && (34 <nav className="nav">35 <button className="btn" onClick={logoutHandler}>36 로그아웃37 </button>38 </nav>39 )}40 </header>41 <main>42 {!isLoggedIn ? (43 <div className="card login">44 <form onSubmit={loginHandler}>45 <div className="control">46 <label htmlFor="email">이메일</label>47 <input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} />48 </div>49 <div className="control">50 <label htmlFor="password">비밀번호</label>51 <input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} />52 </div>53 <div className="actions">54 <button type="submit" className="btn">55 로그인56 </button>57 </div>58 </form>59 </div>60 ) : (61 <div className="card home">62 <h1>환영합니다!</h1>63 <p>로그인에 성공했습니다.</p>64 </div>65 )}66 </main>67 </div>68 );69};70
71export default App;3.3. 코드 분석
3.3.1. 상태(State) 선언
1const [isLoggedIn, setIsLoggedIn] = useState(false);2const [email, setEmail] = useState("");3const [password, setPassword] = useState("");| 변수 | 초기값 | 역할 |
|---|---|---|
isLoggedIn | false | 로그인 여부 — true면 홈 화면, false면 로그인 화면 |
email | "" | 이메일 입력창의 현재 값 |
password | "" | 비밀번호 입력창의 현재 값 |
3.3.2. useEffect — 재방문 체크
1useEffect(() => {2 const storedLoginInfo = localStorage.getItem("isLoggedIn");3 if (storedLoginInfo === "1") {4 setIsLoggedIn(true);5 }6}, []);localStorage는 브라우저에 데이터를 저장하는 공간이다. 새로고침해도 사라지지 않는다.getItem("isLoggedIn")으로 이전에 저장된 로그인 정보를 꺼낸다.- 값이
"1"이면 이미 로그인한 사용자이므로setIsLoggedIn(true)로 상태를 바꾼다. - 의존성 배열이
[]이므로 처음 마운트될 때 한 번만 실행된다 → 무한 루프 없음.
3.3.3. loginHandler — 로그인 처리
1const loginHandler = (e) => {2 e.preventDefault();3 localStorage.setItem("isLoggedIn", "1");4 setIsLoggedIn(true);5};e.preventDefault(): 폼 제출 시 페이지가 새로고침되는 기본 동작을 막는다.localStorage.setItem("isLoggedIn", "1"): 브라우저에 로그인 정보를 저장한다. 재방문해도 로그인 상태가 유지된다.setIsLoggedIn(true): 상태를 바꿔 화면을 홈으로 전환한다.
3.3.4. logoutHandler — 로그아웃 처리
1const logoutHandler = () => {2 localStorage.removeItem("isLoggedIn");3 setIsLoggedIn(false);4 setEmail("");5 setPassword("");6};localStorage.removeItem("isLoggedIn"): 저장된 로그인 정보를 삭제한다.setIsLoggedIn(false): 상태를 바꿔 화면을 로그인 폼으로 전환한다.setEmail(""),setPassword(""): 입력창을 빈 값으로 초기화한다.
3.3.5. JSX — 화면 분기
1{!isLoggedIn ? (2 <로그인 폼 />3) : (4 <환영 메시지 />5)}isLoggedIn이false이면!isLoggedIn이true가 되어 로그인 폼을 보여준다.isLoggedIn이true이면 환영 화면을 보여준다.?:은 삼항 연산자로,if/else를 한 줄로 표현한 것이다.