정의 - 어떠한 의미를 지닌 값.
애플리케이션의 시나리오에 따라 지속적으로 변경될 수 있는 값.
( 출처 : 모던 리액트 deep dive, 김용찬, 위키북스 )
React에서 상태가 변하면 화면이 리렌더링되는 것은 아주 중요한 핵심 원리.
화면에 보여지는 값이나 데이터 등을 상태로 지정해야,
유저 상호 작용이나 시간의 흐름 등에 따라
그 초기값과 업데이트값 등을 적절하게 보여줄 수 있다.
useState로 초기값과 변화 함수를 지정.
Redux Toolkit은 Redux 로직을 작성하기 위해 저희가 공식적으로 추천하는 방법입니다. RTK는 Redux 앱을 만들기에 필수적으로 여기는 패키지와 함수들을 포함합니다. 대부분의 Redux 작업을 단순화하고, 흔한 실수를 방지하며, Redux 앱을 만들기
쉽게 해주는 모범 사례를 통해 만들어졌습니다.
Can I use Redux without Redux toolkit?
: Note that you are not required to use Redux Toolkit to use Redux. There are many existing applications that use other Redux wrapper libraries, or write all Redux logic "by hand", and if you still prefer to use a different approach, go ahead! However, we strongly recommend using Redux Toolkit for all Redux apps
https://ko.redux.js.org/tutorials/essentials/part-1-overview-concepts
https://ko.redux.js.org/tutorials/essentials/part-1-overview-concepts
리덕스란?
: JS앱을 위한 예측 가능한 상태 컨테이너.
'액션'이라고 불리는 이벤트들을 사용해 애플리케이션의 상태를 관리하고 업데이트하는 패턴이자 도구. 애플리케이션 전역에서 필요한 상태들을 관리하기 위해 중앙화된 store(저장소?) 역할을 한다. 상태들이 예측가능한 상황에서만 업데이트되도록 보장한다.
리덕스를 왜 사용할까?
: 애플리케이션의 여러 부분에서 필요한 상태들을 전역적으로 관리할 수 있다. 언제, 어디서, 왜, 어떻게 상태가 업데이트되는지 파악하기 용이.
리덕스를 언제 사용할까?
: 리덕스의 단점으로는, 학습 내용 증가, 길어지는 코드, indirection 추가 (참조, In computer programming, an indirection (also called a reference) is a way of referring to something using a name, reference, or container instead of the value itself), 따라야 하는 제한 사항들을 들 수 있는데,
이러한 리덕스가 유용하게 사용되는 경우는 다음과 같다.
비교적 오랜 기간, 리액트 생태계에서는 상태 관리를 리덕스에 의존했다. 과거 리액트 코드를 보면, 리액트와 리덕스가 함께 설치돼 있는 것을 흔히 볼 수 있었고, 일부 개발자들은 리액트와 리덕스를 마치 하나의 프레임워크 혹은 업계 표준 (de facto)으로 여기기도 했다. 그러나 현재는 새로운 Context API, useReducer, useState의 등장으로 컴포넌트에 걸쳐서 재사용하거나 혹은 컴포넌트 내부에 걸쳐서 상태를 관리할 수 있는 방법들이 점차 등장하기 시작했고, 덕분에 리덕스 이외ㅔ 다른 상태 관리 라이브러리를 선택하는 경우도 많아졌다.
다른 전역 상태 관리 : zustand(useStore로 로컬에서의 상태 및 변화를 관리), Tanstack Query(데이터 페칭, 로딩, 에러 처리와 로컬에서의 상태 및 변화 관리), recoil? zotai?
: 가장 기본적인 방법.
useState의 등장으로 리액트에서는 여러 컴포넌트에 걸쳐 손쉽게 동일한 인터페이스의 상태를 생성하고 관리할 수 있게 되었다.
context API가 선보인 지 1년이 되지 않아, 리액트는 16.8버전에서 함수 컴포넌트에 사용할 수 있는 다양한 훅 API를 추가했다. 이를 통해 제공된 기능 덕분에 기존에 무상태 컴포넌트를 선언하기 위해서 제한적으로만 사용되던 함수 컴포넌트가 클래스 컴포넌트 이상의 인기를 구가하게 되었다. 가장 큰 변경점 중 하나는 state를 매우 손쉽게 재사용 가능하게 만들 수 있다는 점이다.
🖥️ function useCounter() {
const [count, setCount] = useState(0)
function increase() {
setCount((preve) => prev + 1)
}
return { count, increase }
}
useCounter는 단순히 count state아 이를 1씩 올려주는 increase로만 구성돼 있지만 내부적으로 관리하고 있는 state도 있으며, 또 이를 필요한 곳에서 재사용할 수도 있게 됐다. 이는 클래스 컴포넌트보다 훨씬 간결하고 직관적인 방법이었으며, 리액트 개발자들은 앞다투어 자신만의 훅을 만들어내기 시작했다.
이러한 훅과 state의 등장으로 이전에는 볼 수 없던 방식의 상태 관리가 등장하는데, 바로 Tanstack Query와 SWR이다.
둘 다 모두 외부에서 데이터를 불러오는 fetch를 관리하는 데 특화된 라이브러리지만, API 호출에 대한 상태를 관리하고 있기 때문에, HTTP 요청에 특화된 상태 관리 라이브러리라고 볼 수 있다.
SWR과 Tanstack Query가 HTTP 요청에 대해서만 쓸 수 있다면, 좀 더 범용적으로 쓸 수 있는 상태 관리 라이브러리에는 어떤 변화가 있었을까?
요즘 새로 떠오르는 많은 상태 관리 라이브러리는, 기존의 리덕스 같은 라이브러리와는 차이점이 있는데, 바로 훅을 활용해 작은 크기의 상태를 효율적으로 관리한다는 점이다. Recoil, Jotai, Zustand, Valtio의 저장소를 방문해 보면 모두 peerDependencies로 리액트 16.8 버전 이상을 요구하고 있다. 물론 리덕스나 MobX도 react-redux나 mobx-react-lite 등을 설치하면 동일하게 훅을 상태를 가져올 수 있지만, 위 라이브러리는 애초에 리액트와의 연동을 전제로 작동해 별도로 다른 라이브러리를 설치하지 않아도 된다는 차이점이 있다.
이는 기존 상태 관리 라이브러리의 아쉬운 점으로 지적받던 전역 상태 관리 패러다임에서 벗어나 개발자가 원하는 만큼의 상태를 지역적으로 관리하는 것을 가능하게 만들었고, 훅을 지원함으로써 함수 컴포넌트에서 손쉽게 사용할 수 있다는 장점 또한 가지고 있다.
Recoil과 Jotai는 Context와 Provider, 그리고 흑을 기반으로 가능한 작은 상태를 효율적으로 관리하는 데 초점을 맞추고 있다.
🖥️ // Recoil
const counter = atom({ key: 'count', default: 0})
const todoList = useRecoilValue(counter)
: Recoil은 리액트에서 훅의 개념으로 상태 관리를 시작한 최초의 라이브러리 중 하나이며, 최소 상태 개념인 Atom을 처음으로 선보인다.
정식 출시가 아닌 실험적으로 개발되고 운영되는 라이브러리다.
🖥️ // Jotai
const countAtom = atom(0)
const [count, setCount] = useAtom(countAtom)
: Recoil에서 영감을 받았으나 조금 더 유연한 Jotai.
공식 홈페이지에도 나와있듯, Recoil의 atom 모델에 영감을 받아 만들어졌다. Jotai는 상향식(bottom-up) 접근법을 취하고 있다고 나와 있는데, 이는 리덕스와 같이 하나의 큰 상태를 애플리케이션에 내려주는 방식이 아니라, 작은 단위의 상태를 위로 전파할 수 있는 구조를 의미한다. 또한 앞서 언급했던 리액트 Context의 문제점인 불필요한 렌더링을 해결하고자 설계됐으며, 개발자들의 추가적인 메모이제이션이나 최적화를 거치지 않아도 리렌더링이 발생되지 않게 설계돼있다.
Zustand는 리덕스와 비슷하게 하나의 큰 스토어를 기반으로 상태를 관리한다. Recoil, Jotai와는 다르게 이 하나의 큰 스토어는 Context가 아니라 스토어가 가지는 클로저를 기반으로 생성되며, 이 스토어의 상태가 변경되면 이 상태를 구독하고 있는 컴포넌트에 전파해 리렌더링을 알리는 방식이다.
🖥️ // Zustand
const useCounterStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
}))
const count = useCounterStore((state) => state.count)
: 작고 빠르며 확장에도 유연한 Zustand
Jotai가 Recoil의 영감을 받아 만들어졌다면, Zustand는 리덕스에 영감을 받아 만들어졌다. 즉, atom이라는 최소 단위의 상태를 관리하는 것이 아니라, 하나의 스토어를 중앙 집중형으로 활용해 이 스토어 내부에서 상태를 관리하고 있다. 따라서 Zustand를 이해하려면 하나의 큰 스토어가 어떻게 만들어지는지를 먼저 살펴봐야 한다. 스토어를 시작으로, 각 상태값을 어떻게 참조하는지, 또 리렌더링은 어떻게 유도하는지 알아봐야 한다.
Zustand로 간단하게 스토어를 만들고, 이를 사용하는 방법.
🖥️
import { create } from 'zustand'
const useCounterStore = create((set) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set((state) => ({ count: state.count - 1 })),
}))
function counter() {
const { count, inc, dec} = useCounterStore()
return (
<div class="counter">
<span>{count}</span>
<buttton onClick={inc}>up</buttton>
<button onClick={dec}>down</button>
</div>
)
}
위 예제에서는 Zustand의 create를 사용해 스토어를 만들고, 반환 값으로 이 스토어를 컴포넌트 내부에서 사용할 수 있는 훅을 받았다. 그리고 이 훅을 사용하면 스토어 내부에 있는 getter 모두에 접근해 사용할 수 있게 된다.
또 리액트 컴포넌트 외부에 store를 만드는 것도 가능하다.
import { createStore, useStore } from "zustand";
const counterStore = createStore((set) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
dec: () => set((state) => ({ count: state.count - 1 })),
}));
function Counter() {
const { count, inc, dec } = useStore(counterStore);
return (
<div class="counter">
<span>{count}</span>
<buttton onClick={inc}>up</buttton>
<button onClick={dec}>down</button>
</div>
);
}
createStore를 사용하면 리액트와 상관 없는 바닐라 스토어를 만들 수 있으며, 이 바닐라 스토어는 useStore훅을 통해 접근해 리액트 컴포넌트 내부에서 사용할 수 있게 된다.
특징
🖥️ // Valtio
const state = proxy({ count: 0 })
const snap = useSnapshot(state)
state.count++
( 출처 : 모던 리액트 deep dive, 김용찬, 위키북스 )
버츄얼 돔은 실제로 실행되기 전에 그 미리 한번 그려보고
그러는 걸로 아는데요 어..
브라우저의 렌더링 과정에 대해서 설명해주세요.
- 답변)
- 의도: 단순히 자바스크립트, 리액트 등의 코딩 기술이 아니라 전반적인 프론트엔드 지식을 가지고 있는지 확인하는 질문.
- 팁: 순서에 관련한 질문이기에 제스처 등 비언어적 표현으로 보조하는 것도 좋습니다.
- 모범 답안: 브라우저의 렌더링 과정은 크게 HTML 파싱 - CSS 파싱 - DOM 트리와 CSSOM 트리 결합 - 레이아웃 계산 - 페인팅 단계로 이루어집니다. HTML을 파싱해 DOM 트리를 만들고, 비슷하게 CSS를 파싱해 CSSOM 트리를 만듭니다. 이후에 두 트리를 결합하고 페이지에 렌더링 하기 위해 필요한 공간을 계산합니다. 마지막으로 레이아웃에 따라 화면에 요소를 페인팅합니다.
DOM : Document Object Model의 약자로, 브라우저가 웹페이지 어떻게 그릴 것인지 정보를 가지고 있다.
useRef는 useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도
변경 가능한 상태값을 저장한다는 공통점이 있다.
그러나 useState와 구별되는 큰 차이점 두 개가 있다.
렌더링에 영향을 미치지 않는 고정된 값을 관리하게 위해서라면, 함수 외부에서 값을 선언해 관리하는 것이 동일한 기능을 수행할 수 있지 않을까?
🖥️
let value = 0;
function Component() {
function handleClick() {
value += 1
}
//...
}
이 방식에는 몇 가지 단점이 있다.
➡️ useRef는 상기 두 가지 문제를 극복할 수 있는 리액트식 접근법.
컴포넌트가 렌더링될 때만 생성되며, 컴포넌트 인스턴스가 여러 개라도
각각 별개의 값을 바라본다.
🖥️ useRef를 사용한 DOM 접근 예제
function RefComponent() {
const inputRef = useRef()
//이때는 렌더링이 실행되기 전(반환 전)이므로 undefined를 반환
console.log(inputRef.current) //undefined
useEffect(() => {
console.log(inputRef.current) //<input type="text"></input>
}, [inputRef])
return <input ref={inputRef} type="text" />
}
useRef는 최초에 넘겨받은 기본값을 가지고 있다.
명심할 것은, useRef의 최초 기본값은 return문에 정의해 둔 DOM이 아니고 useRef()로 넘겨받은 인수라는 점이다.
useRef가 선언된 당시에는 아직 컴포넌트가 렌더링되기 전이라, return으로 컴포넌트의 DOM이 반환되기 전이므로 undefined다.
useRef를 사용할 수 있는 유용한 경우는, 렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있다는 특징을 활용해 useState의 이전 값을 저장하는 userPrevious() 같은 훅을 구현할 때다.
🖥️ useRef를 활용한 usePrevious 훅 구현
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]); //value가 변경되면 그 값을 ref에 넣어둔다
function SomeComponent() {
const [counter, setCounter] = useState(0);
const previousCounter = usePrevious(counter);
function handleClick() {
setCounter((prev) => prev + 1);
}
// 0 (undefined)
// 1, 0
// 2, 1
// 3, 2
return (
<button onClick={handleClick}>
{counter}
{previousCounter}
</button>
);
}
}
➡️ 이렇게 개발자가 원하는 시점의 값을, 렌더링에 영향을 미치지 않고 보관해두고 싶다면 useRef를 사용하는 것이 좋다.
그렇다면 useRef는 어떻게 구현되어 있을까?
🖥️ React에서의 useRef 구현
export function useRef(initialValue) {
currentHook = 5
return useMemo(() => ({current: initialValue}), [])
}
값이 변경되어도 렌더링되면 안 된다는 점, 실제 값은 {current: value}와 같은 객체 형태로 있다는 점을 떠올려 보자. 렌더링에 영향을 미치면 안 되기 때문에 useMemo에 의도적으로 빈 배열을 선언해 뒀고, 이는 각 렌더링마다 동일한 객체를 가리키는 결과를 낳을 것이다. 자바스크립트의 특징, 객체의 값을 변경해도 객체를 가리키는 주소가 변경되지 않는다는 것을 떠올리면 useMemo로 useRef를 구현할 수 있다.
( 출처 : 모던 리액트 deep dive, 김용찬, 위키북스 )
마운트 - useEffect... 으어어엉
var : 쓰지 마세요
let : 가변적 변수 선언, 재할당 가능, 코드 어디선가 이 변수의 값이 다르게 할당될 가능성
const : 불변성 유지 변수 선언, 재할당 불가능, 근데 상수라고 불러야만 정확한 건가?
var : 스코프 내 중복 선언 가능, 재할당 가능, 변수 선언이 무조건 상위로 끌어올려지는 호이스팅 발생 (런타임 이전에 변수 선언과 초기화 단계를 실행), 선언과 초기화 동시에 이루어짐, 함수 레벨 스코프
let : 스코프 내 중복 선언 불가능(Syantax Error 발생), 재할당 가능, 호이스팅 발생하지 않는 것처럼 동작 (엄밀히 따지면 호이스팅 O), 선언과 초기화 따로 이루어짐, 블록 레벨 스코프 : 블록 (중괄호로 나뉘는 for문, if문, switch문 등)을 다 지역 스코프로 취급해 동명의 변수가 스코프에 따라 다르게 사용될 수 있음, 값 재할당 가능,
const : 중복 선언, 재할당 불가능한 상수 (값의 불변을 의미
하지는 않는다), 예를 들어 객체 할당 시 값 변경
( 출처 : 모던 자바스립트 deep dive, 이웅모, 위키북스 )