발표
3주차 발표
고도연

발표자료

클린코드 리액트(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할 필요도 없다.
  • 컴포넌트 내부에서의 변수는?
    • 렌더링마다 고유의 값을 가지는 계산된 값

💡

  1. props를 useState에 넣지 않고 바로 return 문에 사용하기
  2. 컴포넌트 내부 변수는 렌더링마다 고유한 값을 가짐
  3. 따라서 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;