섹션 7. State 고급스킬
-객체 State 업데이트하기
- 리액트 state가 가진 기존의 객체를 직접 변경하면 안됨
- 객체를 업데이트 하고 싶을 때는 새로운 객체 생성하여 state가 복사본을 사용하도록 해야 함
여러 필드에 단일 이벤트 핸들러 사용하기
const handleTitleChange = (e) => {
setForm({
...form,
title: e.target.value })
}
const handleDescriptionChange = (e) => {
setForm({
...form,
description: e.target.value })
} 에서
const handleChange = (e) => {
console.log('e.target.name: ', e.target.name)
setForm({
...form,
[e.target.name]: e.target.value })
} 동적으로 변경
...form은 기존 form 객체의 모든 속성을 복사
-Deep Copy vs. Shallow Copy
...(스프레드) 전개 문법은 한 레벨 깊이의 내용만 복사함. 중첩된 프로퍼티는 딥카피가 이루어지지 않음
- 중첩 객체 업데이트
state가 깊이 중첩되어있다면 평탄화 해볼 수 있다
{
"id": 0,
"title": "Earth",
"childIds": [
{
"id": 1,
"title": "Asia",
"childIds": [
{
"id": 2,
"title": "Korea",
"childIds": []
},
{
"id": 3,
"title": "Japan",
"childIds": []
}
]
},
{
"id": 4,
"title": "Europe",
"childIds": []
}
]
}
"Korea" 노드를 찾으려면 Asia를 거쳐서 찾아야 함.
{
"0": { "id": 0, "title": "Earth", "childIds": [1, 4] },
"1": { "id": 1, "title": "Asia", "childIds": [2, 3] },
"2": { "id": 2, "title": "Korea", "childIds": [] },
"3": { "id": 3, "title": "Japan", "childIds": [] },
"4": { "id": 4, "title": "Europe", "childIds": [] }
}
->객체의 키를 기준으로 평탄화함. 배열[2]하면 "Korea" 정보를 바로 가져올 수 있음
하지만 이는 state의 구조를 바꿔야하므로 immer를 사용하여 간단하게 로직을 작성해볼 수도 있음
immer 사용하기
- immer는 내부적으로 draft의 어느 부분이 변경되었는지 알아내어, 변경사항을 포함한 완전히 새로운 객체를 생성
- Immer를 사용하기 위해서는,
- package.json에 dependencies로 use-immer를 추가
- npm install을 실행
- import { useState } from 'react'를 import { useImmer } from 'use-immer'로 교체, useState를 useImmer로 교체
npm i use-immer
**draft는 Immer 라이브러리에서 제공하는 프록시(proxy) 객체다
📌 Immer에서 draft란?
- draft는 원본 상태를 직접 변경하는 것처럼 보이지만, 실제로는 불변성을 유지하면서 변경을 적용할 수 있도록 도와주는 가짜(임시) 상태
- Immer 내부에서 프록시 객체로 관리되며, 변경이 끝나면 자동으로 새로운 불변 객체를 생성해줌
updatePerson(draft => { draft.artwork.city = 'Lagos'; });
-배열 업데이트 하기
- 배열은 JavaScript에서는 변경이 가능하지만, state로 저장할 때에는 변경할 수 없도록 처리해야 한다.(읽기 전용)
- 객체와 마찬가지로, state에 저장된 배열을 업데이트하고 싶을 때에는, 새 배열을 생성(혹은 기존 배열의 복사본을 생성)한 뒤, 이 새 배열을 state로 두어 업데이트해야 한다.
setArtists( // 아래의 새로운 배열로 state를 변경합니다.
[
...artists, // 기존 배열의 모든 항목에,
{ id: nextId++, name: name } // 마지막에 새 항목을 추가합니다.
]
);
📌 배열에서 항목을 제거하는 filter()
filter()는 배열(Array)의 내장 함수 중 하나로, 특정 조건을 만족하는 요소만 남긴 새로운 배열을 반환하는 함수
const newArray = oldArray.filter((element) => 조건);
- oldArray: 기존 배열
- element: 배열의 각 요소
- 조건: true를 반환하는 요소만 새로운 배열에 포함됨
- newArray: 조건을 만족하는 요소들로 이루어진 새로운 배열
📌 배열에서 항목을 교체하는 map()
const newArray = oldArray.map((element) => {
// 각 요소를 변환하여 새로운 배열에 추가
return 변환된 값;
});
📌배열에 항목을 삽입하는 slice()
function handleClick() {
const insertAt = 1; // 모든 인덱스가 될 수 있습니다.
const nextArtists = [
// 삽입 지점 이전 항목
...artists.slice(0, insertAt),
// 새 항목
{ id: nextId++, name: name },
// 삽입 지점 이후 항목
...artists.slice(insertAt)
];
setArtists(nextArtists);
setName('');
}
📌배열에 기타변경 적용
JavaScript의 reverse() 및 sort() 함수는 원본 배열을 변경시키므로 직접 사용할 수 없다.
대신, 먼저 배열을 복사한 뒤 변경할 수 있다.
function handleClick() {
const nextList = [...list];
nextList.reverse();
setList(nextList);
}
-immer로 배열 내부의 객체 업데이트하기
- Immer를 사용하면 artwork.seen = nextSeen과 같이 기존 객체를 참조해도 된다
- 원본 state를 변경하는 것이 아니라, Immer에서 제공하는 특수 draft 객체를 변경하기 때문
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
- 최신 Array API로 업데이트 하기
원본 배열을 변경하지 않고 새로운 배열을 반환하는 메서드들
- Array.prototype.toSorted
- Array.prototype.toReversed
- Array.prototype.toSpliced : 지정된 요소를 제거하거나 새로운 요소를 추가한 새로운 배열을 반환
- Array.prototype.with : 지정된 인덱스의 요소를 새로운 값으로 대체한 새로운 배열을 반환
섹션 8. Reducer로 로직 통합
-State로직을 리듀서로 작성하기
- 한 컴포넌트에서 state업데이트가 여러 이벤트 핸들러로 분산될 때 state를 업데이트하는 모든 로직을 reducer를 사용하여 컴포넌트 외부로 단일 함수로 통합할 수 있다
- reducer함수에서는 타입에 따라 비즈니스 로직을 구성할 때 switch문을 사용하는 것이 일반적
- useState에서 useReducer로 바꾸기
1. state를 설정하는 것에서 action을 dispatch 함수로 전달하는 것으로 바꾸기.
function handleDeleteTask(taskId) {
dispatch(
// "action" 객체:
{
type: 'deleted',
id: taskId
}
);
}
dispatch 함수에 넣어준 객체를 “action” 이라고 함. 일반적으로 어떤 상황이 발생하는지에 대한 최소한의 정보를 담고 있어야 함. dispatch는 단순히 신호를 보내는 역할이고, 실제 상태 변경 로직은 reducer에서 관리됨.
2. reducer 함수 작성하기
function yourReducer(state, action) {
// React가 설정하게될 다음 state 값을 반환합니다.
}
- 첫 번째 인자에 현재 state (tasks) 선언하기.
- 두 번째 인자에 action 객체 선언하기.
- reducer에서 다음 state 반환하기 (React가 state에 설정하게 될 값).
3. 컴포넌트에서 reducer 사용하기
import { useReducer } from 'react';
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer hook은 두 개의 인자를 넘겨받는다
- reducer 함수
- 초기 state 값
그리고 아래와 같이 반환한
- state를 담을 수 있는 값
- dispatch 함수 (사용자의 action을 reducer 함수에게 “전달하게 될”)
- Reducer에 Immer의 간결함을 더하기
- reducer는 순수해야 하기 때문에, 이 안에서는 state를 변경할 수 없다.
- 그러나, Immer에서 제공하는 특별한 draft 객체를 사용하면 안전하게 state를 변경할 수 있다.
- Immer는 변경 사항이 반영된 draft로 state의 복사본을 생성함
- immer사용-> break로 빠져나가기 잊지말기
import { useImmerReducer } from 'use-immer';
섹션 9. 이벤트 심화
-리액트 이벤트 객체
- SyntheticEvent: 리액트는 모든 이벤트를 SyntheticEvent 객체로 래핑한다. 이 객체는 브라우저 간 호환성을 보장하고, 성능을 최적화하기 위해 이벤트 풀링(pooling)을 사용한다.
- nativeEvent: SyntheticEvent 객체의 nativeEvent 속성은 원래의 네이티브 브라우저 이벤트 객체를 가리킨다. 이 객체를 통해 브라우저에서 발생한 실제 이벤트에 대한 모든 정보에 접근할 수 있다.
-한글 이슈
- 한글 입력 시 Enter 키를 누르면 함수가 두 번 호출되는 이슈는 주로 IME(Input Method Editor)와 관련이 있다
- IME는 한글과 같은 다중 문자 언어를 입력할 때 사용되는 시스템이다
- IME를 사용하면 사용자가 글자를 조합하는 동안 여러 키 입력 이벤트가 발생합니다. 이 과정에서 Enter 키가 입력 확정 및 조합 완료 신호로도 사용되기 때문에 문제가 발생한다.
- 해결방법 :
if (e.key === 'Enter' && e.nativeEvent.isComposing === false) {
// 등록하는 로직 실행
}
섹션 10. Context API 기초 & 심화
-Context: Props 전달하기의 대안
- props 전달하기는 어떤 prop을 트리를 통해 깊이 전해줘야 하거나, 많은 컴포넌트에서 같은 prop이 필요한 경우에 불편할 수 있고, “Prop drilling”이라는 상황을 초래할 수도 있다
- Context는 부모 컴포넌트가 그 아래의 트리 전체에 데이터를 전달할 수 있도록 해준다
- export const MyContext = createContext(defaultValue)로 context를 생성하고 내보내기
- useContext(MyContext) Hook에 전달해 얼마나 깊이 있든 자식 컴포넌트가 읽을 수 있도록 하기
- 자식을 <MyContext.Provider value={...}>로 감싸 부모로부터 context를 받도록 하기
- 예시
아래의 코드는 각각의 섹션에 level을 수동으로 지정해야 한다.
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Context를 이용하면 위의 컴포넌트에서 정보를 읽을 수 있으므로 각 Section은 위의 Section에서 level을 읽고 자동으로 level + 1을 아래로 전달할 수 있다.
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext value={level + 1}>
{children}
</LevelContext>
</section>
);
}
context API를 이용할 때는 provider를 이용해서 부모 컴포넌트에서 자식 컴포넌트로 값을 전달할 수 있다.
섹션 11. 라이프사이클 & Effect Hook
-React Hook
: 리액트에서 Hooks(훅)은 리액트 버전 16.8부터 도입된 기능으로, 함수형 컴포넌트에서 상태 관리와 라이프사이클 메서드를 사용할 수 있게 해주는 함수들이다.
- useState로 상태 관리
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
- useEffect로 부수 효과(side effect)를 관리. 컴포넌트가 마운트, 업데이트, 언마운트될 때 특정 작업을 수행할 수 있다.
** React에서 컴포넌트가 마운트(mount)된다는 것은 컴포넌트가 처음으로 화면에 나타나는 순간을 의미함
즉, 컴포넌트가 생성되어 DOM에 추가될 때를 말하는 것
import React, { useEffect } from 'react';
function Timer() {
useEffect(() => {
const timer = setInterval(() => {
console.log('타이머 작동 중...');
}, 1000);
// 클린업 함수
return () => clearInterval(timer);
}, []);
ㅈ
return <div>타이머가 실행 중입니다.</div>;
}
- useContext로 컨텍스트(Context)를 사용하여 상태를 전달
import React, { useContext } from 'react';
const ThemeContext = React.createContext('light');
function ThemedComponent() {
const theme = useContext(ThemeContext);
return <div>Current theme: {theme}</div>;
}
리액트 훅의 규칙
- 훅은 최상위 레벨에서만 호출해야 한다( 반복문, 조건문, 중첩된 함수 내에서 훅 호출 금지 )
- 훅은 리액트 함수 내에서만 호출해야 한다
-React 라이프사이클
: 리액트에서 라이프사이클이란 컴포넌트가 생성되어 DOM에 삽입되고, 업데이트되며, 제거될 때까지의 일련의 과정을 말한다. 함수형 컴포넌트에서는 useEffect 훅을 사용하여 이러한 라이프사이클을 관리함.
섹션 12. useRef Hook
-useRef
: useRef 는 렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook이다
useRef 에 의해 저장된 값은 컴포넌트의 렌더링과 무관하게 유지되며, 값을 변경하더라도 리렌더링이 발생하지 않는다.(useState와의 차이점)
const ref = useRef(initialValue)
- initialValue : ref 객체의 current프로퍼티 초기 설정값. 이 인자는 초기 렌더링 이후부터는 무시됨
- useRef는 current라는 단일 프로퍼티를 가진 객체를 반환한다.
- current: 처음에는 전달한 initialValue로 설정되고 나중에 다른 값으로 바꿀 수 있다. ref 객체를 JSX 노드의 ref어트리뷰트로 React에 전달하면 React는 current프로퍼티를 설정한다.
-값을 변경 하더라도 리렌더링이 발생하지 않는 let 키워드와 다른 점? → useRef는 컴포넌트별로 저장공간 할당함!!
- 렌더링할 때마다 재설정되는 일반 변수와 달리) 리렌더링 사이에 정보를 저장할 수 있다.
- (리렌더링을 촉발하는 state 변수와 달리) 변경해도 리렌더링을 촉발하지 않는다.
- (정보가 공유되는 외부 변수와 달리) 각각의 컴포넌트에 로컬로 저장된다.
-ref로 DOM 조작하기
- 초기값이 null인 ref객체를 선언한다
- ref 객체를 ref 속성으로 조작하려는 DOM 노드의 JSX에 전달한다
- React가 DOM 노드를 생성하고 화면에 그린 후, React는 ref 객체의 current 프로퍼티를 DOM 노드로 설정한다. 이제 DOM 노드 <input>에 접근하여 focus()와 같은 메서드를 호출할 수 있다.
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
// ...
// ...
return <input ref={inputRef} />;
function handleClick() {
inputRef.current.focus();
}
섹션 13. 성능 최적화
-useMemo Hook
- React에서 성능 최적화를 위해 제공하는 Hook
- 메모이제이션(memoization)을 통해 연산량이 많은 작업이나 자주 변경되지 않는 값을 재계산하는 비용을 줄여준다 == 컴포넌트가 리렌더링될 때 계산 결과를 캐싱할 수 있게 해준다 **캐싱한다는 것은 이전 결과를 재사용한다는 뜻
- 이전 계산 결과를 저장하고, 같은 입력값이면 저장된 값을 사용하는 것
- useMemo(calculateValue, dependencies) 일 때 calculateValue는 계산을 수행하는 함수, dependencies는 종속성 목록(이 값이 변경되면 다시 계산해야 함)
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo는 종속성 목록인 a와 b가 변경될 때에만 computeExpensiveValue 함수를 다시 실행. a와 b가 변경되지 않았다면, 이전에 계산된 값 그대로 반환
-주요 특징
- 성능 최적화: 복잡한 계산이나 데이터 처리 로직이 포함된 경우, useMemo를 사용하여 불필요한 재계산을 방지
- 의존성 배열: 두 번째 인자인 의존성 배열이 변경될 때만 메모이제이션된 값을 갱신. 이 배열이 비어 있으면, 해당 값은 컴포넌트가 처음 렌더링될 때 한 번만 계산됨.
- 불변성 보장: 메모이제이션된 값을 재사용함으로써 동일한 입력에 대해 항상 동일한 출력이 보장.
-주의 사항
- useMemo는 Hook이므로 컴포넌트의 최상위 레벨 또는 자체 Hook에서만 호출할 수 있다. 반복문이나 조건문 내부에서는 호출할 수 없다.
- Strict Mode의 디버깅 기능을 피하기 위해 순수하게 작성할 것
- useMemo를 썼을 때, 그 결과값이 의존성이 바뀌지 않으면 계속 유지되기 때문에 의존성 배열을 정확히 넣을 것
-Effect내에서 사용하기
모든 반응형 값은 Effect의 종속성으로 선언되어야 한다.
종속성 목록을 선언하면 렌더링마다 변경될 것이기 때문에, Effect 안에서 사용되는 객체를 useMemo로 감싸면 된다.
const options = useMemo(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ roomId가 변경될때만 실행됩니다.
...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ options가 변경될때만 실행됩니다.
// ...
-useCallback Hook
- useCallback Hook은 React에서 컴포넌트가 리렌더링될 때마다 함수를 새로 만들지 않도록 막아주는 역할을 한다.
- 성능 최적화를 위해 주로 사용되는데, 특히 자식 컴포넌트에 콜백 함수를 props로 넘길 때 유용함
const memoizedCallback = useCallback(() => {
// 실행될 코드
}, [dependencies]);
- useCallback은 첫 번째 인자로 받은 함수를 기억(memoization) 함
- 두 번째 인자인 dependencies(의존성 배열)에 있는 값이 바뀔 때만 함수를 새로 생성함
- 의존성 배열이 변하지 않으면 이전에 만든 함수를 그대로 재사용
-useMemo vs. useCallback Hook
| useCallback | useMemo | |
| ✅ 목적 | 함수를 메모이제이션 | **값(연산 결과)**를 메모이제이션 |
| 🧠 반환값 | 콜백 함수 자체 | 콜백 함수의 리턴값 |
| 🧩 문법 | useCallback(fn, deps) | useMemo(fn, deps) |
| 🛠️ 사용 예 | 함수를 props로 넘겨줄 때, 불필요한 리렌더 방지 | 복잡한 계산 결과를 캐싱할 때 사용 |
'프론트엔드' 카테고리의 다른 글
| [React] 영화 목록 웹사이트 만들기 + 검색 기능 구현 (1) | 2025.04.27 |
|---|---|
| [React] 린캔버스 프로젝트 (0) | 2025.04.06 |
| [React]React 완벽 마스터: 기초 개념부터 린캔버스 프로젝트까지 ① (0) | 2025.03.22 |
| [HTML, CSS] 네이버 클론코딩 ② (0) | 2025.02.18 |
| [HTML, CSS] 네이버 클론코딩 ① (0) | 2025.02.09 |