home

State

글 분류
react bible
키워드
react
생성일
2023/06/10 00:57
최근 수정일
2023/12/20 23:32
작성중
컴포넌트에서 변할 때 마다 렌더링 하는 기준이 되는 값

기본

initializer, updater가 두번 실행된다

function TodoList() { // This component function will run twice for every render. const [todos, setTodos] = useState(() => { // This initializer function will run twice during initialization. return createTodos(); }); function handleClick() { setTodos(prevTodos => { // This updater function will run twice for every click. return [...prevTodos, createTodo()]; }); } // ...
TypeScript
복사
effects와 같은 현상으로 dev + strict 모드에서 우발적인 비순수성을 검사하기위해 두번 실행된다. → 순수함수임을 검사하기 위함이다. 순수 함수일 경우 두번 실행되도 같은 값이 실행 되야 하므로.
두개의 initializer 호출 중 하나의 호출은 무시된다.

이전 값을 참조할 때는 항상 updater function을 사용해야할까?

이전 값을 참조하는 state 최신화 시 항상 updater function 사용이 필수는 아니다.
대부분의 사용 케이스에서는 updater function 사용이나 기존 state 값 참조는 별차이가 없다.
하지만, 하나의 이벤트핸들러 또는 함수에서 여러번의 이전 값 참조 setState가 이루어질 때 또는 state 참조가 복잡할때는 updater 함수가 더 간편하다.
필요에 따라 원하는 setState 관련 로직을 작성해도 괜찮지만 코드의 일관성을 중요시 할 경우 항상 updater function을 사용하는것 또한 좋은 방법

일반 변수는 왜 rendering을 유도하지 않는가?

일반 변수로 값 변화시 아무것도 발생하지 않는다, rerendering이 발생하지 않는다.
그렇다고 일반 변수 변화 할 때 마다 rerendering 된다면? → 그많은 값의 변화에 맞춰서 rerendering 되면 퍼포먼스 저하가 당연
일반 변수는 추가로 렌더간에 값이 유지되지 않는다 → 리렌더링 될때마다 초기화
리액트에서 일반 variable은 렌더간 값 유지가 아닌 간단 계산을 위한 결과로만 사용하도록 디자인 되어있다.

렌더링 도중 만나는 setState

import { useState } from 'react'; export default function CountLabel({ count }) { const [prevCount, setPrevCount] = useState(count); const [trend, setTrend] = useState(null); if (prevCount !== count) { setPrevCount(count); setTrend(count > prevCount ? 'increasing' : 'decreasing'); return; } return ( <> <h1>{count}</h1> {trend && <p>The count is {trend}</p>} </> ); }
TypeScript
복사
렌더링 도중에 setState를 수행하는 패턴은 가독성이 떨어지며 이해하기 어려운 코드이므로 대부분의 케이스에서 피해야할 패턴이다.
하지만 effect에 넣어 update 하는것 보단 좋은로직 → effect는 children까지 렌더링 하고 다시 렌더하므로
렌더링 도중 setState를 만날경우 react는 children을 렌더하지 않고 해당 컴포넌트의 리렌더링만 수행한 후 새로운 리렌더링을(자식을 포함한) 수행한다.
결과적으로 자식을 두번 수행하지 않으므로 불필요한 렌더링을 멈춘다.
조건문이 모든 hook 호출문 아래에 있을 경우 early return을 이용해 좀더 빠르게 렌더링을 유도할 수 있다.

state는 렌더링 당시의 스냅샷

우리가 보는 화면은 state 변화로 인해 리렌더링 될 당시의 snapshot
리렌더 된다 → 컴포넌트의 함수를 불러 결과값을 생성 → 그 생성된 결과값이 존재하는 그 당시의 snapshot(but interactive)

batching + asynchronous

이벤트 핸들러 내부에서 state 값은 고정된다 (기본적으론, 물론 튜닝가능)
예시코드
state 값 자체는 렌더중에 변경되지 않음
첫 렌더로 인한 Counter 컴포넌트의 snapshot 상태에는 number은 0였음
setState를 만났다고 바로 이루어지는게 아님 → 해당 onClick 함수 실행 완료 후 마지막에 모든 setState를 batching
batching이 뭐에요?

updater function

새로운 값으로 replace하는게 아닌 기존 state 값을 이용한 변경이 필요할 경우 사용하는 함수
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); }}>+3</button> </> ) }
Bash
복사
uncommon use case
batching은 여전히 수행됨 → batching 무효화 업데이트를 이용해 3번 리렌더링 따로하는것과는 전혀 다른 개념
렌더링 간 작동하는 함수 → 순수함수여야 하며 같은 input 같은 result가 기본 r규칙
event handler가 실행될 때 리액트는 모든 updater function을 모든 큐에 넣고 event handler가 실행되고 난 후 실행
event handler가 실행된 후 렌더링 시 큐에있는 updater function을 전부 실행 시켜 최종 값을 적용 시킨다.
setState(5); setState(n => 5);
Bash
복사
setState(5)는 실제로 setState(n ⇒ 5)의 형태로 작용됨 (n은 사용되지 않을 뿐)
updater function을 쓰면 리액트가 이벤트핸들러 작동시 updater function 확인해서 다른 형태로 작동하는구나! → X
항상 updater function 형태로 작동, 간단한 형태로 표현할 뿐 리액트는 항상 이벤트 핸들러 내부의 setState를 큐에 저장하여 핸들러 실행 완료 후 렌더링 시 큐에 담긴 updater function을 한번에 적용 → O
// state 변화 후 리렌더링 시 state 최종 값 결정 함수의 쉬운예시 export function getFinalState(baseState, queue) { let finalState = baseState; for (let update of queue) { if (typeof update === 'function') { // Apply the updater function. finalState = update(finalState); } else { // Replace the next state. finalState = update; } } return finalState; }
TypeScript
복사

당연하게도 이벤트 루프는

예시코드
send로 보내고 5초후에 alert 동작 → 그 5초 사이에 리렌더링 되며 state가 변경되면 어떻게 될까?
당연히, 또 당연히 closure로 인해 setTimeout이 set되던 당시의 state값으로 alert 수행
해당 함수가 생성됐던 환경을 기억하므로 EventLoop으로 삽입될 당시의 값을 가지고 있음

state의 정신

imperative(명령형)

“핸들을 좌로 35도 꺾은다음 엑셀 21% 정도로 밟아. 그리고 우측 미러 확인 후 뒤에 차를 확인해. 만약 뒤에차가 32미터 이상 뒤에 있다면 차선을 우로 변경하는데 그때는 엑셀을 이전 에 비해 32프로 정도 더 밟아서…“
예시코드
독립된, 작은 코드스페이스에선 코드관리의 어려움이 크지 않음 → 커지면 커질수록 불지옥이 되어간다

declarative(선언형)

“드림플러스 강남점으로 가주세요”
React에서는 UI를 명령형으로 직접 조작하지 않는다. → 컴포넌트를 직접 활성화, 비활성화, 표시 또는 숨기지 않고 표현하려는 데이터를 state로 선언하여 관리한다.

선언적인 UI 구성 접근법

1. 한 페이지에서 바뀌는 시각적 상태 파악

input 페이지에 필요한 state를 파악하는 단계
오류
작성 중
성공

2. state 변화 trigger 확인하기

state 변화를 trigger하는 이벤트를 파악하는 단계
user input → 작성 중
network response → 오류, 성공
time change → 예시

3. useState를 이용하여 필요한 state들 작성

const [isEmpty, setIsEmpty] = useState(true); const [isTyping, setIsTyping] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isSuccess, setIsSuccess] = useState(false); const [isError, setIsError] = useState(false); const [error, setError] = useState(null);
TypeScript
복사
복잡할 경우 많은 생각 없이 1차원적으로 접근, 필요한 state 전부 작성

4. 필수적이지 않은 state 제거

유효하지 않은 형태의 UI를 유저에게 보여주지 않아야 한다. → input창 disable 되어있는데 error는 나와있고 typing 중 이라 떠있는 불지옥을… 유저에게 경험하게 해주고 싶은가?
1.
해당 state들이 역설을 유발하는가?
isTyping, isSubmitting은 둘다 true가 될수 없다. → 역설
isTyping, isSubmitting, isSubmit 이라는 상태를 전부 따로 두는 것은 명령형에 가까운 접근법이 아닌가 → “typing 중에는 isTyping을 true로 변경하고 isSubmitting을 false로 바꾸고 ….”
inputStatus라는 하나의 state로 typing, submitting, success라는 값을 주는것이 더 선언적이고 관리가 쉽다.
2.
해당 정보를 다른 state가 이미 가지고 있는가?
isEmpty - input 값이 0인지 확인하는 state
answer - 정답을 저장하는 state
answer.length === 0 코드로 isEmpty를 대신할 수 있다, isEmpty는 이 코드로 대치될 수 있지 않은가? → isEmpty 제거
isError는 error !== null로 확인할 수 있다 → isError 제거
다이어트 후 코드
const [isEmpty, setIsEmpty] = useState(true); const [isTyping, setIsTyping] = useState(false); const [isError, setIsError] = useState(false); const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
TypeScript
복사

5. 이벤트 핸들러 연결하기

말.그.대.로 이벤트 핸들러에 setState 연결한다.

state 구조 만들기

업데이트나 참조를 편하게 하고 실수를 최소화 하기 위한 state 구조 만들기

그룹화

// AS-IS const [x, setX] = useState(0); const [y, setY] = useState(0); // TO-BE const [position, setPosition] = useState({ x: 0, y: 0 });
TypeScript
복사
두개 이상의 나뉘어져 있는 state를 한번에 업데이트한다? → 그걸 왜 나눠 두냐;; 하나로 합쳐 ㅋ

모순제거

// AS-IS const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); // TO-BE const [status, setStatus] = useState('typing');
TypeScript
복사
모순이(오류) 발생 할 수 있는 상황 자체를 만들지 말자.
위 코드는 물론 실수이겠지만 isSent, isSending 둘 다 true가 되는 모순이 발생하는 상황이 될 수 있다.
status 하나로 통일 해버리면 에러가 발생할 가능성 자체가 사라진다.

불필요한 state 제거

// AS-IS const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); // TO-BE const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName;
TypeScript
복사
특정 state를 다른 state로 부터 추론 할 수 있다면 굳이 state로 또 만들 필요는 없다 → 뭐하러 더 복잡하게 만드는가?

중복제거

특정 state를 다른 state로 부터 추론 할 수 있다면 굳이 state로 또 만들 필요는 없다 → 뭐하러 더 복잡하게 만드는가?

중첩제거

깊은 중첩으로 존재하는 state는 update도 힘들고 참조도 힘들다. 최대한 flat한 구조를 사용하자.

참조