컴포넌트에서 변할 때 마다 렌더링 하는 기준이 되는 값
기본
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한 구조를 사용하자.