- Published on
Reading React Official Documents (2)
- Authors
- Name
- Easyoon
[React] Official Docs memo 2
무엇을 state로 관리할 것인지 등
컴포넌트 개발 전 고민
1. Mock-Up (JSON 등)으로 시작
2. 화면을 UI 컴포넌트로 구성할 때
간단한 경우 : 하층부에 있는 컴포넌트에서 상층부에 있 는 컴포넌트의 순서로 개발
프로젝트가 큰 경우 : 상향식으로 만들고 테스트를 작성하는 쪽이 빠름
3. Props & State 판단
부모로부터 props를 통해 전달하는 값 => props
시간이 지나도 변하지 않는 값=> props
state나 props를 가지고 계산 가능한 값 => props
제품의 원본 목록은 props를 통해 전달되므로 state가 아님
검색어와 체크박스는 state, 시간이 지남에 따라 변하기도 하면서 다른 것들로부터 계산될 수 없다.
필터링 된 목록 => state가 아님. 제품의 원본 목록과 검색어, 체크박스의 값을 조합해서 계산해낼 수 있기 때문
4. 애플리케이션이 가지는 각각의 state에 대해
state를 기반으로 렌더링하는 모든 컴포넌트 찾기
공통 소유 컴포넌트 (common owner component)를 찾으세요. (계층 구조 내에서 특정 state가 있어야 하는 모든 컴포넌트들의 상위에 있는 하나의 컴포넌트)
**공통 혹은 더 상위에 있는 컴포넌트가 state를 가진다 **(만약, state를 소유 할 적절한 컴포넌트를 찾지 못하였다면, state를 소유하는 컴포넌트를 하나 만들어서 공통 소유 컴포넌트의 상위 계층에 추가)
5. Result
state로 관리할 부분: 검색어와 체크박스 상태 표시
ProductTable : state에 의존한 상품 리스트의 필터링 SearchBar : 검색어와 체크박스의 상태를 표시 FilterableProductTable : 위 두 컴포넌트를 소유한 공통 소유 컴포넌트
- 의미상으로도 FilterableProductTable이 검색어와 체크박스의 체크 여부를 가지는 것이 타당
this.state = {filterText: , inStockOnly: false}
를 FilterableProductTable의 constructor에 추가하여 애플리케이션의 초기 상태를 반영 후, filterText와 inStockOnly를 ProductTable와 SearchBar에 prop으로 전달하는 식.
setState()
- props를통해 콜백 함수를 전달하는 것이 일반적
updateCount() {
this.setState((prevState, props) => {
return { count: prevState.count + 1 }
});
}
// DO NOT USE
this.setState({
count: this.state.count + 1
});
setState상태를 직접 업데이트하는 대신 콜백 함수를 가져와야 함. 콜백 내에서 prevState액세스 가능
initialState
initialState 인자는 초기 렌더링 시에 사용하는 state로, 그 이후의 렌더링 시에는 이 값은 사용 되지 않음
초기 state가 고비용 계산의 결과라면, 초기 렌더링 시에만 실행될 함수를 대신 제공할 수 있습니다.
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
Hook
왜 Vue나 React가 재사용 가능한 동작을 구성 요소에 “부착”하는 방법(예: 저장소에 연결)에서 hooks로 넘어가는가
: 구성 요소 간에 상태 저장 논리를 재사용하기 어려워서
Hooks를 사용하면 컴포넌트에서 상태를 추출하여 독립적으로 테스트하고 재사용 가능
Hooks를 사용하면 컴포넌트 계층 구조를 변경하지 않고도 컴포넌트간에 쉽게 공유 가능
useEffect Hook
컴포넌트의 렌더링 이후 다양한 사이드 이펙트를 제어
정리(clean-up)를 이용하는 Effects hook
Effect에 정리(clean-up)가 필요한 경우에는 함수를 return
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
정리(clean-up)가 필요없는 경우
useEffect(() => {
document.title = `You clicked ${count} times`;
});
Tip) Effect 내 로직이 많이 필요한 경우, 구분을 위해서 useEffect를 여러번 구분해서 호출
Tip) Effect를 건너뛰어 성능 최적화
: props.friend.id 값이 변경 될 경우에만 재렌더링
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // props.friend.id가 바뀔 때만 재구독합니다.
- 주의
배열이 컴포넌트 범위 내에서 바뀌는 값들과 effect에 의해 사용되는 값들을 모두 포함해야 함
그렇지 않으면 현재 값이 아닌 이전의 렌더링 때의 값을 참고
effect를 실행하고 이를 정리(clean-up)하는 과정을 (마운트와 마운트 해제 시에)딱 한 번씩만 실행하고 싶다면, 빈 배열([])을 두 번째 인수로 넘김
*아래 참고
function Example({ someProp }) {
function doSomething() {
console.log(someProp); }
useEffect(() => {
doSomething();
}, []);
}
// 🔴 이것은 안전하지 않습니다 (`someProp`을 사용하는`doSomething`을 호출합니다)}
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp); }
doSomething();
}, [someProp]); // ✅ OK (우리 effect는`someProp` 만 사용합니다)}
useEffect(() => {
function doSomething() {
console.log('hello');
}
doSomething();
}, []); // ✅ 이 예에서는 컴포넌트 범위의 *어떤* 값도 사용하지 않기 때문에 좋습니다
}
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
// productId props 사용
const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 🔴 `fetchProduct`가`productId`를 사용하므로 잘못되었습니다
}
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// 이 함수 컴포넌트를 effect 내부로 이동하면 사용하는 값을 명확하게 볼 수 있습니다.
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ 효과는 productId 만 사용하므로 유효합니다
}
Tip) Hook Linter Plugin
Hook을 사용할 때는 두 가지 규칙을 준수해야 하기 때문에, 이 규칙들을 자동으로 강제하기 위한 linter 플러그인을 제공
반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하지 말 것
(렌더링이 될 때마다동일한 순서로 Hook이 호출되는 것을 보장)
// 🔴 조건문에 Hook을 사용함으로써 첫 번째 규칙을 깼습니다
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
조건부로 effect를 실행하기를 원한다면, 조건문을 Hook 내부에 넣으면 됨
useEffect(function persistForm() {
// 👍 더 이상 첫 번째 규칙을 어기지 않습니다
if (name !== '') {
localStorage.setItem('formData', name);
}
});
useReducer
useState의 대체 함수로 다음 2가지 경우에 사용
다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우
다음 state가 이전 state에 의존적인 경우
또한 useReducer는 자세한 업데이트를 트리거 하는 컴포넌트의 성능을 최적화할 수 있게 하는데, 이것은** 콜백 대신 dispatch를 전달 할 수 있기 때문**
(** 자세한 셈플 필요, reduce()를 dispatch와 함께 사용하는 예시)
아래는 useState 를 reducer를 사용해서 다시 작성한 예시 (모르겠음)
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
메모이제이션된 값을 반환 (Vue로 따지면 Computed 개념)
useMemo로 전달된 함수는 렌더링 중에 실행 = 통상적으로 렌더링 중에는 하면 안되는 로직들을 넣으면 안됨 (렌더링 관련 로직은 UseEffect)
useImperativeHandle
사용을 지양 대부분의 경우 좀 더 나은 컴포넌트 설계로 필요한 이슈들을 해결할 수 있다.
useLayoutEffect
개념은 useEffect와 동일하긴 한데, 모든 DOM 변경 후에 동기적으로 발생, 따라서 DOM에서 레이아웃을 읽고 동기적으로 리렌더링하는 경우에 사용
먼저 useEffect를 사용해 보고 문제가 있다면 그다음으로 useLayoutEffect를 사용해 보기를 권장
+)서버 렌더링을 사용하는 경우라면 자바스크립트가 모두 다운로드될 때까지는 useLayoutEffect와 useEffect 어느 것도 실행되지 않는다