발표자료
클린코드 리액트(React) (opens in a new tab) 강의를 보고 정리한 내용입니다.
1. 올바른 초기값 설정
초기값
- 가장 먼저 렌더링될 때 순간적으로 보여질 수 있는 값이기도 하다.
- 당연히 초기에 렌더링 되는 값
- list의 값을 map으로 돌땐 초기 값(빈배열)을 잘 지정해줘야 쓸데없이 렌더링 하지 않는다.
- useState가 비어있을 경우에는 undefined를 가지기 때문에 초기값을 정해주는 것이 좋다.
// ❌
const [list, setList] = useState();
list.map((item) => (
<Item item={item} />
))
// ⭕️
const [list, setList] = useState([]);
list.map((item) => (
<Item item={item} />
))
- 지키지 않을 경우? ⇒ 렌더링 이슈, 무한 루프, 타입 불일치로 의도치 않는 동작 => 런타임 에러
- 초기값을 넣지 않으면? ⇒ undefined
- 상태를 CRUD ⇒ 상태를 지울 때도 초기값을 잘 기억해놔야 원상태로 돌아간다.
- null 처리를 할 때 ⇒ 불필요한 방어코드도 줄여준다.
2. 업데이트되지 않는 값
- 리액트 컴포넌트 내부에 직접적으로 업데이트되지 않는 변수를 가지고 있으면, 매번 렌더링 될 때마다 변하지 않는 값인데 불필요하게 참조하게 된다.
- 리액트 상태로 바꾼다거나 아예 외부로 내보내도록 수정하는 것이 좋다
// ❌
// 상수를 다루거나 일반적인 방치
// 업데이트가 되지 않는 일반적인 객체
function component() {
const INFO = {
name: 'My Component',
value: 'Clean Code React',
};
return (
<div>
<showCount info={INFO} />
</div>
);
}
// ⭕️
const INFO = {
name: 'My Component',
value: 'Clean Code React',
};
function component() {
return (
<div>
<showCount info={INFO} />
</div>
);
}
💡 업데이트가 되지 않는 일반적인 객체(상수나 고유한 값)라면 리액트 외부로 내보내기
3. 플래그 상태
플래그 값
프로그래밍에서 주로 특정 조건 혹은 제어를 위한 조건을 boolean으로 나타내는 값
ex) 로그인, 인증/인가
- 플래그 값을 만들땐 굳이 상태를 만들지 말고 표현식으로 하나씩 넣으면 useState를 사용하지 않고 컴포넌트 내부의 변수를 사용하여 만들 수 있다.
// ❌
function FlagState() {
const [isLogin, setIsLogin] = useState(false);
useEffect(() => {
if (hasToken) {
setIsLogin(true);
}
if (hasCookie) {
setIsLogin(true);
}
if (isValidCookie) {
setIsLogin(true);
}
if (isValidToken) {
setIsLogin(true);
}
}, [hasToken, hasCookie, isValidCookie, isValidToken]);
return <div>{isLogin && '안녕하세요! 반갑습니다.'}</div>;
}
// ⭕️
function FlagState() {
const isLogin =
hasToken &&
hasCookie &&
isValidCookie &&
isValidToken;
return <div>{isLogin && '안녕하세요! 반갑습니다.'}</div>;
}
💡 useState 대신 플래그로 상태를 정의할 수 있다.
4. 불필요한 상태
- 초기값을 바꿔야 하거나 변하는 경우에 사용된다.
// ❌
// 1. 초기 상태 선언
const [userList, setUserList] = useState(MOCK_DATA);
// 2. 변경 후 저장할 데이터 선언
const [complUserList, setComplUserList] = useState(MOCK_DATA);
useEffect(() => {
const newList = complUserList.filter(
(user) => user.completed === true,
);
setUserList(newList);
}, [userList]);
// ⭕️
// 1. 초기 상태 선언
const [userList, setUserList] = useState(MOCK_DATA);
// 2. 변경 후 저장할 데이터 선언 (내부의 변수로 사용)
const complUserList = complUserList.filter(
(user) => user.completed === true,
);
- 렌더링될 때마다 complUserList가 생성되기 때문에 관리할 필요도 없고 set할 필요도 없다.
- 컴포넌트 내부에서의 변수는?
- 렌더링마다 고유의 값을 가지는 계산된 값
💡
- props를 useState에 넣지 않고 바로 return 문에 사용하기
- 컴포넌트 내부 변수는 렌더링마다 고유한 값을 가짐
- 따라서 useState가 아닌 const로 상태를 선언하는게 좋은 경우도 있음
5. useState 대신 useRef
// ❌
export const component = () => {
const [isMount, setIsMount] = useState(false);
useEffect(() => {
if(!isMount) {
setIsMount(true);
}
}, [isMount]);
};
// ⭕️
export const component = () => {
const isMount = useRef(false);
useEffect(() => {
isMount.current = true;
return () => (isMount.current = false);
}, []);
};
- 리렌더링 방지가 필요하다면 useState 대신 useRef
- 컴포넌트의 전체적인 수명과 동일하게 지속된 정보를 일관적으로 제공해야하는 경우
💡 useState 대신 useRef를 사용하면 컴포넌트의 생명주기와 동일한 리렌더링되지 않는 상태를 만들 수 있다
6. 연관된 상태 단순화하기
// ❌
import React from "react";
function FlatState() {
const [isLoading, setIsLoading] = useState(false);
const [isFinish, setIsFinish] = useState(false);
const [isError, setIsError] = useState(false);
const fetchData = async () => {
// fetch Data 시도
setIsLoading(true);
fetch(url)
.then(() => {
// fetch Data 성공
setIsLoading(false);
setIsFinish(true);
})
.catch(() => {
// fetch Data 실패
setIsError(true);
});
};
if (isLoading) return <LoadingComponent />;
if (isFinish) return <SuccessComponent />;
if (isError) return <ErrorComponent />;
}
export default FlatState;
잠재적 문제
isLoading, isFinish, isError 모두 연관이 있다. (ex. isLoading이 true일 경우는 나머지 상태는 모두 false)
해결 방법 : 하나로 합치기
- ✅ 열거형 데이터로 바꾼다. => 6. 연관된 상태 단순화하기
- 객체로 합친다. => 7. 연관된 상태 객체로 묶어내기
- 객체로 합쳐서 reducer로 묶는다. => 8. useState에서 useReducer로 리팩터링
import React from "react";
const PROMISE_STATE = {
INIT: "init",
LOADING: "loading",
FINISH: "finish",
ERROR: "error",
}
function FlatState() {
const [promiseState, setPromiseState] = useState(PROMISE_STATE.INIT)
const fetchData = async () => {
// fetch Data 시도
setPromiseState(PROMISE_STATE.LOADING);
fetch(url)
.then(() => {
// fetch Data 성공
setPromiseState(PROMISE_STATE.FINISH);
})
.catch(() => {
// fetch Data 실패
setPromiseState(PROMISE_STATE.ERROR);
});
};
if (promiseState === PROMISE_STATE.LOADING) return <LoadingComponent />;
if (promiseState === PROMISE_STATE.FINISH) return <SuccessComponent />;
if (promiseState === PROMISE_STATE.ERROR) return <ErrorComponent />;
}
export default FlatState;
7. 연관된 상태 객체로 묶어내기
하나의 상태가 바뀔 때 나머지 상태를 동기화해줘야 한다.
import React from "react";
function FlatState() {
const [fetchState, setFetchState] = useState({
isLoading: false,
isFinish: false,
isError: false,
})
const fetchData = async () => {
// fetch Data 시도
setFetchState({
isLoading: true,
isFinish: false,
isError: false,
});
fetch(url)
.then(() => {
// fetch Data 성공
setFetchState({
isLoading: false,
isFinish: true,
isError: false,
})
})
.catch(() => {
// fetch Data 실패
setFetchState({
isLoading: false,
isFinish: false,
isError: true,
})
});
};
if (fetchState.isLoading) return <LoadingComponent />;
if (fetchState.isFinish) return <SuccessComponent />;
if (fetchState.isError) return <ErrorComponent />;
}
export default FlatState;
이전 상태를 참고하는 prevState를 spread operator를 사용하여 반복되는 코드를 줄여줄 수 있다.
import React from "react";
function FlatState() {
const [fetchState, setFetchState] = useState({
isLoading: false,
isFinish: false,
isError: false,
});
const fetchData = async () => {
// fetch Data 시도
setFetchState((prevState) => ({
...prevState,
isLoading: true,
}));
fetch(url)
.then(() => {
// fetch Data 성공
setFetchState((prevState) => ({
...prevState,
isFinish: true,
}));
})
.catch(() => {
// fetch Data 실패
setFetchState((prevState) => ({
...prevState,
isError: true,
}));
});
};
if (fetchState.isLoading) return <LoadingComponent />;
if (fetchState.isFinish) return <SuccessComponent />;
if (fetchState.isError) return <ErrorComponent />;
}
export default FlatState;
하나의 상태만 useState로 사용할 필요 없고 여러 상태도 useState로 사용할 수 있다.
8. useState에서 useReducer로 리팩터링
import React from "react";
const INIT_STATE = {
isLoading: false,
isSuccess: false,
isFail: false,
};
const reducer = (state, action) => {
switch (action.type) {
case "FETCH_LOADING":
return { ...state, isLoading: true };
case "FETCH_SUCCESS":
return { ...state, isSuccess: true };
case "FETCH_FAIL":
return { ...state, isFail: true };
default:
return INIT_STATE;
}
};
function StateToReducer() {
const [state, dispatch] = useReducer(reducer, INIT_STATE);
const fetchData = async () => {
// fetch Data 시도
dispatch({ type: "FETCH_LOADING" });
fetch(url)
.then(() => {
// fetch Data 성공
dispatch({ type: "FETCH_SUCCESS" });
})
.catch(() => {
// fetch Data 실패
dispatch({ type: "FETCH_FAIL" });
});
};
if (state.isLoading) return <LoadingComponent />;
if (state.isSuccess) return <SuccessComponent />;
if (state.isFail) return <FailComponent />;
}
export default StateToReducer;
action type을 통해 내부로직을 추상화해주기 때문에 type만 보고도 추론이 가능하다.
더 많은 상태가 있어도 reducer에서 관리해줄 수 있다.
9. 상태 로직 Custom Hooks으로 뽑아내기
import React, { useEffect } from "react";
const INIT_STATE = {
isLoading: false,
isSuccess: false,
isFail: false,
};
const reducer = (state, action) => {
switch (action.type) {
case "FETCH_LOADING":
return { ...state, isLoading: true };
case "FETCH_SUCCESS":
return { ...state, isSuccess: true };
case "FETCH_FAIL":
return { ...state, isFail: true };
default:
return INIT_STATE;
}
};
const useFetchData = (url) => {
const [state, dispatch] = useReducer(reducer, INIT_STATE);
useEffect(() => {
const fetchData = async () => {
// fetch Data 시도
dispatch({ type: "FETCH_LOADING" });
await fetch(url)
.then(() => {
// fetch Data 성공
dispatch({ type: "FETCH_SUCCESS" });
})
.catch(() => {
// fetch Data 실패
dispatch({ type: "FETCH_FAIL" });
});
};
fetchData();
}, [url]);
return state;
};
function CustomHooks() {
const { isLoading, isSuccess, isFail } = useFetchData("url");
if (isLoading) return <LoadingComponent />;
if (isSuccess) return <SuccessComponent />;
if (isFail) return <FailComponent />;
}
export default StateToReducer;