Skip to main content

React 공식문서

1.UI 표현하기

1.7 리스트 렌더링

https://ko.react.dev/learn/rendering-lists

1.즉석에서 key를 만들기 보다는, 데이터 안에 key를 포함해야 한다.
2.서버데이터 => DB의 PK, 로컬데이터 => uuid 같은 패키지를 사용.

  • 리액트는 키를 사용하지 않으면 배열의 인덱스를 사용 한다.
  • key={Math.random()}처럼 즉석에서 key를 생성하지 말기, 모든 컴포넌트와 DOM이 매번 다시 생성 및 내부 상태 손실.
  • 자식 컴포넌트 배열이 정적인 경우라면 index를 사용해도 좋다.
  • 단, 혹시라도 자식 컴포넌트 중 state가 관리된다면 예기치 못한 버그가 생길것이다.

3.State 관리하기

3.4 State를 보존하고 초기화하기 : 리액트 상태 랜더 트리의 위치(자리)가 중요하다.

📌 리액트에서 key란?

  • 리액트 엘리먼트가 고유함을 식별하는데 사용, 재생성 대신 리렌더링, 생명주기 유지로 DOM 효율적 업데이트.
  • *React는 당신이 반환하는 컴포넌트 트리를 기준으로 본다.
  • 부모 안에서의 순서 변경은 다른 컴포넌트로 본다.

1.같은 위치의 같은 컴포넌트는 state를 보존한다.

  • 예) 컴포넌트 트리는 유지한 채로 state만 변경하는 경우 = 카운터

2.같은 위치의 다른 컴포넌트는 state를 초기화 한다.

  • 예) 카운터를 show, hide하는 경우 카운터 내부 state는 초기화.
  • 컴포넌트 함수를 중첩해서 정의하면 안 되는 이유입니다.
  • 컴포넌트 내 컴포넌트를 만든다 = 매번 다른 컴포넌트를 생성한다. 라는 의미이다.
  • 중첩 컴포넌트는 매번 라이프싸이클을 다시 시작한다.

3.같은 위치의 다른 key를 가진 컴포넌트는 state를 초기화 한다.

  • props가 변경되는것은 리랜더링의 대상이지, 컴포넌트 재생성을 위해서는 key를 변경해야 한다.

4.부모의 자식의 컴포넌트 리스트는 렌더링이 최적화 된다.

  • 4.1 key가 없는경우, 자식 컴포넌트 리스트의 위치기반으로 리렌더링 된다.
  • 4.2 key가 있는경우, 자식 컴포넌트 리스트의 key가 동일하면 상태를 보존해준다.
  • 4.3 key가 있는경우, 자식 컴포넌트 리스트의 key가 다르다면 상태를 독립적으로 관리해준다.

📌 챌린지 도전 - 2 (https://ko.react.dev/learn/preserving-and-resetting-state)

  • 현상 : input필드의 상태는 유지되면서, label만 변경되고 있다.
  • 즉, label 상태만 리렌더링되고 input 컴포넌트와 state는 유지되는 중. (모든 컴포넌트가 리렌더)

Case1. 순서를 변경해도 label만 변경되는 경우.
Case2. 순서를 변경해도 input이 유지되는 경우.
Case3. 순서를 변경해도 input이 초기화되는 경우.

📌 챌린지 도전 - 5 (https://ko.react.dev/learn/preserving-and-resetting-state)

  • 이슈 : key를 index로 사용해서 문제가 발생하는 경우.
  • 해결 : key값을 email로 설정하여 독립적인 state 관리 단위를 만들자.

📌 Uncontrolled Components

  • React 리렌더링을 안하면서, 많은 form을 관리해야 하는 경우 응용가능하다.
  • 초기값 : defaultValue, defaultChecked 를 사용.
  • Get Input Value : ref를 이용해서 입력값들을 수집한다.

4.탈출구

4.1 Ref로 값 참조하기.

ref의 사용목적

📌 ref의 사용목적 : 컴포넌트가 리렌더링 사이의 일부정보를 유지 & 렌더링 유발하지 않게 하기 위함.

  • *렌더링 로직에 영향을 미치지 않는 경우 사용한다.
  • *state는 snapshot처럼 동작한다. 이와 상관없이 최신의 정보를 참조하고 싶을때 사용.

📌 useState와 ref의 차이

  • ref는 mutable 가능, state는 immutable 로 리렌더 대기열 넣어야 함.

예) 타이머의 interval ID 값,

  • interval ID은 리렌더링에 상관없다.
  • 이벤트 핸들러가 취소하기 위해서 Interval ID 정보를 기억해야한다. Ref를 사용하자.

useState로 useRef 구현하기.

  • ref.current의 이유는 불변성 때문이다.
// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}

📌 refs를 사용하는 useCase
1.timeout ID 저장
2.DOM 엘리먼트 저장 및 조작
3.JSX를 계산하는데 필요하지 않은 다른 객체 저장.

📌 timeout ID 저장을 useRef 대신 컴포넌트 외부에 값을 저장해도 작동하긴 한다. 차이점은?

  • 컴포넌트 생명주기와 관련이 있다.
  • useRef는 컴포넌트 인스턴스마다 생기는 독립적인 값.
  • 컴포넌트 외부 값은 모든 컴포넌트가 공유하는 static한 글로벌 변수이다.

4.2 Ref로 DOM 조작하기

useRef, ref callback, forwardRef, useImperativeHandle, flushSync

1.예시, input focus 하기

  function handleClick() {
inputRef.current.focus();
}

2.예시, scrollIntoView

  function handleScrollToFirstCat() {
firstCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}

3.예시, ref콜백

  • ref콜백은 라이프싸이클에 맞추어서 node 혹은 null을 전달해준다.
{catList.map((cat) => (
<li
key={cat}
ref={(node) => {
const map = getMap();
node ? map.set(cat, node) :map.delete(cat);
}}
>
<img src={cat} />
</li>
))}

📌 forwardRef

  • 자식에게 ref를 전달가능
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});

📌 useImperativeHandle

  • 부모 컴포넌트에서 ref를 이용해서 예상치 못 한 작업을 방지하는 법.
  • 아래 예시는 부모 컴포넌트는 ref를 통해서 focus만 가능하다.
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 오직 focus만 노출합니다.
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});

📌 flushSync

  • state는 큐에 쌓여 비동기로 처리된다.
  • 핸들러 함수에서는 DOM업데이트전의 상태를 보고 있다.
  • 아래 예시처럼 최신의 DOM을 보고 DOM API를 호출하려면 flushSync로 커밋페이스까지 동기화 가능.
  function handleAdd() {
const newTodo = { id: nextId++, text: text };
flushSync(() => {
setText('');
setTodos([ ...todos, newTodo]);
});
// By this line, the DOM is updated.
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}

📌 React가 관리하는 DOM 노드, ref로 관리하는 DOM 노드

  • 차트 라이브러리 등 직접 DOM 관리를 해야하는 경우라면 React랑 충돌을 피해야 한다.
  • 빈 div 태그를 리턴하고, 해당 태그안에서 DOM을 조작하자.

4.8 커스텀 Hook으로 로직 재사용하기

📌 커스텀 Hook 이란?

  • 리액트 라이프싸이클 훅이 포함된 재사용가능한 로직.

📌 커스텀 Hook이 구체적인 고급 사용 사례에 집중하도록 하기

1.이상적으로 커스텀 Hook의 이름은 코드를 자주 작성하는 사람이 아니더라도

  • 커스텀 Hook이 무슨 일을 하고, 무엇을 props로 받고, 무엇을 반환하는지 알 수 있도록 아주 명확해야 합니다.
  • ✅ useData(url)
  • ✅ useImpressionLog(eventName, extraData)
  • ✅ useChatRoom(options)

2.외부 시스템과 동기화할 때, 커스텀 Hook의 이름은 좀 더 기술적이고 해당 시스템을 특정하는 용어를 사용하는 것이 좋습니다.

  • 해당 시스템에 친숙한 사람에게도 명확한 이름이라면 좋습니다.
  • ✅ useMediaQuery(query)
  • ✅ useSocket(url)
  • ✅ useIntersectionObserver(ref, options)

3.커스텀 Hook이 구체적인 고급 사용 사례에 집중할 수 있도록 하세요.

  • useEffect API 그 자체를 위한 대책이나 편리하게 감싸는 용도로 동작하는 커스텀 “생명 주기” Hook을 생성하거나 사용하는 것을 피하세요.
  • 🔴 useMount(fn)
  • 🔴 useEffectOnce(fn)
  • 🔴 useUpdateEffect(fn)

📌 useSyncExternalStore


📌 requestAnimationFrame

배경지식

  • 보통 초당 60번 화면을 그리는데, 리프레시 주기에 맞추어 로직 작성 > 브라우저 성능을 최적화
  • 콜백 함수는 17ms간격으로 호출.
  • 만약 requestAnimationFrame 안의 작업이 33ms 걸린다면 '프레임 드랍' 발생, 30fps 갱신을 보여준다.
    • 장점 : 탭이 비활성화 > 자동 일시정지 된다.

참고 - performance.now()

const time1 = performance.now();
for (let i = 1; i <= 1_000_000_000; i++) {}
const time2 = performance.now();
console.log(time2 - time1); // 약 350ms

예시) useFadeIn

import { useEffect } from 'react';

export function useFadeIn(ref, duration) {
useEffect(() => {
const node = ref.current;

let startTime = performance.now();
let frameId = null;

function onFrame(now) {
const timePassed = now - startTime;
const progress = Math.min(timePassed / duration, 1);
onProgress(progress);
if (progress < 1) {
// 아직 그려야 할 프레임이 많습니다.
frameId = requestAnimationFrame(onFrame);
}
}

function onProgress(progress) {
node.style.opacity = progress;
}

function start() {
onProgress(0);
startTime = performance.now();
frameId = requestAnimationFrame(onFrame);
}

function stop() {
cancelAnimationFrame(frameId);
startTime = null;
frameId = null;
}

start();
return () => stop();
}, [ref, duration]);
}

예시) useFadeIn with Class

export class FadeInAnimation {
private node: HTMLElement;
private duration: number = 1000; // 1sec delay
private startTime: number | null = null;
private frameId: number | null = null;

constructor(node: HTMLElement) {
this.node = node;
}
start(duration: number) {
this.duration = duration;
this.startTime = performance.now();
this.onProgress(0);
// (x) this.frameId = requestAnimationFrame(this.onFrame);
// (x) this.frameId = requestAnimationFrame(() => this.onFrame);
this.frameId = requestAnimationFrame(() => this.onFrame());
}
stop(): void {
if (this.frameId) cancelAnimationFrame(this.frameId);
this.startTime = null;
this.duration = 1;
this.frameId = null;
}
private onFrame() {
if (!this.startTime) return;

const timePassed = performance.now() - this.startTime;
const progress = Math.min(timePassed / this.duration, 1);

this.onProgress(progress);
if (progress < 1) {
this.frameId = requestAnimationFrame(() => this.onFrame());
} else {
this.stop();
}
}
private onProgress(progress: number) {
this.node.style.opacity = progress.toString();
}
}

---
import { useEffect, RefObject } from 'react';
import { FadeInAnimation } from './FadeInAnimation';

export function useFadeIn(ref: RefObject<HTMLElement>, duration: number): void {
useEffect(() => {
if (!ref.current) return;
const animation = new FadeInAnimation(ref.current);
animation.start(duration);

return () => {
animation.stop();
};
}, [ref, duration]);
}

📌 useEffectEvent(실험)

  • 아직 실험적인 기능이다.
  • 사용 목적 : useEffect 에서 의존성배열 추가하지 않고 핸들러 함수를 호출하고 싶을때

예) 챌린지 도전하기 4번

import { useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export function useInterval(onTick, delay) {
useEffect(() => {
const id = setInterval(onTick, delay);
return () => {
clearInterval(id);
};
}, [onTick, delay]);
}
---
import { useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export function useInterval(callback, delay) {
const onTick = useEffectEvent(callback);
useEffect(() => {
const id = setInterval(onTick, delay);
return () => clearInterval(id);
}, [delay]);
}