프로젝트 이모저모

[투두리스트 페어프로그래밍] 지역변수, useRef, useState 차이

Ella Seon 2023. 7. 5. 09:54

0. 배경 설명

- Todo 메인 페이지 컴포넌트에서 아침/점심/저녁 버튼을 누르면 아침(빨간색 포스트잇)/점심(노란색 포스트잇)/저녁(파란색 포스트잇) 으로 시간별로 분류되는 기능을 구현하려고 했다.

- 우리는 이번프로젝트에서는 전역상태관리를 쓰지 않기로 해서, Todo 컴포넌트에서 todolist state값을 useNavigate를 이용해서 Category 컴포넌트로 전달을 했다.

 

todolist state의 초기값은 배열이 되고 todolist 의 항목들이 객체형태로 배열에 담긴다. 아래처럼 되어있다.

{
  "id": 1,
  "todo": "Hello World",
  "isCompleted": true,
  "userId": 2
}

 

useNavigate로 todolist 의 상태값을 Category 컴포넌트로 전달했다.

더불어 timeCategory 라고 아침,점심,저녁 버튼의 값도 전달했다.

//Todo.tsx
const handleViewCategory = (e: MouseEvent<HTMLButtonElement>) => {
    const timeCategory = e.currentTarget.innerText;
    navigate("/todo/category", {
      state: {
        type: timeCategory,
        data: todoList,
      },
    });
  };

 

1. 버그 현황 (timeState 지역 변수 값 재할당 되지 않는 이슈)

- useNavigate로 전달한 데이터를 useLocation() 으로 받아왔다.

- 그래서 timeState라는 변수를 설정하고 state.type 즉 버튼의 글자인 아침/점심/저녁 일경우에 페이지의 h1 태그가 morning, afternoon, evening으로 바뀌게 설정하려고 했다.

- 헌데, timeState 변수는 반영이 되지 않았고, 콘솔창에도 아무런 것도 나타나지 않았다. 

function Category() {
  const navigate = useNavigate();
  const { state } = useLocation();
  const [categoryTodoList, setCategoryTodoList] = useState([...state.data]);

  let timeState = "";
  console.log(timeState);

  useEffect(() => {
    if (state.type === "아침") {
      timeState = "morning";
      setCategoryTodoList(
        categoryTodoList.filter((postIt: TodoItem) => {
          return postIt.todo.slice(-1) === "1";
        })
      );
    } else if (state.type === "점심") {
      timeState = "afternoon";
      setCategoryTodoList(
        categoryTodoList.filter((postIt: TodoItem) => {
          return postIt.todo.slice(-1) === "2";
        })
      );
    } else if (state.type === "저녁") {
      timeState = "evening";
      setCategoryTodoList(
        categoryTodoList.filter((postIt: TodoItem) => {
          return postIt.todo.slice(-1) === "3";
        })
      );
    }
  }, []);

  return (
    <div className="bg-main_skyblue flex flex-col justify-center items-center h-screen">
      <section className="bg-main_bg_cloud max-w-7xl w-98 mb-5 rounded-xl h-600 relative">
        <div className="sticky top-0 pb-5 rounded-t-xl bg-main_bg_cloud ">
          <h1 className="font-mono pt-9 text-4xl text-center font-bold">
            {timeState}
          </h1>
          <ul className="h-fit max-h-450 pt-5 pb-5 pr-10 pl-10 grid grid-cols-2 gap-4 overflow-y-scroll">
            {categoryTodoList.map((postIt: TodoItem) => {
              return (
                <PostItem
                  key={postIt.id}
                  todoId={postIt.id}
                  todoList={categoryTodoList}
                  setTodoList={setCategoryTodoList}
                  isCompleted={state.data}
                >
                  {postIt.todo}
                </PostItem>
              );
            })}
          </ul>
        </div>
      </section>
      <Button
        size="large"
        onClick={() => {
          navigate("/todo");
        }}
      >
        이전으로
      </Button>
    </div>
  );
}

export default Category;

 

2. 버그 해결

1️⃣useEffect 내부에서 timeState를 재할당 해주고 있었다. 

생각 해보니 변수는 값이 바뀌어도 재 렌더링을 해주지 않는다.   리액트는 state, props 변경에 따라 리렌더링이 된다. 변수를 재 할당하더라도 리액트는 컴포넌트의 재 렌더링을 해주지 않는다. timeState는 단순히 함수 컴포넌트 안에 선언된 지역 변수이다. timeState를 재 할당 하더라도 이를 감지하고 컴포넌트를 다시 렌더링 하지 않는다. 

 

더불어 컴포넌트가 재렌더링 되면 함수컴포넌트 내부에 선언된 모든 변수들은 초기화된다. 따라서 계속 console.log()에 아무런 값도 못보여주고 있었던 것이다. 

 

setCategoryTodolist 라는 setState 함수는 상태가 변경이 되니 리액트가 이를 감지하고 화면에 보여줬지만, 

timeState는 변수라서 반영을 해주지 못했다. 

 

2️⃣ timeState를 useState에 담아보자!

 

잘 변경이 된다. 

const [timeState, setTimeState] = useState("");

  console.log(timeState);

  useEffect(() => {
    if (state.type === "아침") {
      setTimeState("morning");
      setCategoryTodoList(
        categoryTodoList.filter((postIt: TodoItem) => {
          return postIt.todo.slice(-1) === "1";
        })
      );
    } else if (state.type === "점심") {
      setTimeState("afternoon");
      setCategoryTodoList(
        categoryTodoList.filter((postIt: TodoItem) => {
          return postIt.todo.slice(-1) === "2";
        })
      );
    } else if (state.type === "저녁") {
      setTimeState("evening");
      setCategoryTodoList(
        categoryTodoList.filter((postIt: TodoItem) => {
          return postIt.todo.slice(-1) === "3";
        })
      );
    }
  }, []);

하지만 우리는 state.type에 따라 h1 태그를 변동하는 것이다. h1 태그는 아침 버튼일 경우 'morning'으로 글자가 변동되면 더이상 변동될 필요가 없다. 따라서 재 렌더링이 되지 않는 useRef로 변수를 관리해보자.

 

3️⃣timeState를 useRef 에 담아보자

- let timeState ='' 지역변수로 관리했을 때는 컴포넌트가 재 렌더링 되면 값이 초기화가 되었었다. 하지만 , useRef 는 리렌더링이 되어도 값을 유지한다. 

- 그리고, useRef 는 state와 달리 current 속성이 이 변경되어도 컴포넌트를 재 렌더링 하지 않는다. 따라서 재 렌더링 없이 값을 유지해야할 때 예를 들면 input 요소에 접근하거나, 타이머 ID를 저장하거나, 이전 state 또는 prop 값을 기억하는 등의 상황에서 쓰인다.  즉, useRef의 값이 변경되어도 React가 이를 추적하지 않는 다는 의미이다. 

 

.current 속성을 이용해보자

current 값은 선택한 DOM 을 가리키게 된다. 

function Category() {
  const navigate = useNavigate();
  const { state } = useLocation();
  const [categoryTodoList, setCategoryTodoList] = useState([...state.data]);

  const timeState = useRef("");
  console.log(timeState);

  useEffect(() => {
    if (state.type === "아침") {
      timeState.current = "morning";
      setCategoryTodoList(
        categoryTodoList.filter((postIt: TodoItem) => {
          return postIt.todo.slice(-1) === "1";
        })
      );
    } else if (state.type === "점심") {
      timeState.current = "afternoon";
      setCategoryTodoList(
        categoryTodoList.filter((postIt: TodoItem) => {
          return postIt.todo.slice(-1) === "2";
        })
      );
    } else if (state.type === "저녁") {
      timeState.current = "evening";
      setCategoryTodoList(
        categoryTodoList.filter((postIt: TodoItem) => {
          return postIt.todo.slice(-1) === "3";
        })
      );
    }
  }, []);

  return (
    <div className="bg-main_skyblue flex flex-col justify-center items-center h-screen">
      <section className="bg-main_bg_cloud max-w-7xl w-98 mb-5 rounded-xl h-600 relative">
        <div className="sticky top-0 pb-5 rounded-t-xl bg-main_bg_cloud ">
          <h1 className="font-mono pt-9 text-4xl text-center font-bold">
            {timeState.current}
          </h1>
          {/*중략*/}
    </div>
  );
}

export default Category;

 

4️⃣이 상황에서는 useRef 를 쓰는것이 좋을까 useState를 쓰는 것이 좋을까?

- useState 로 timeState를 설정하고   console.log(timeState); 를 찍어보니 삭제 버튼을 눌렀을때 다른 컴포넌트에 의해 상태가 변경되니 재 렌더링이 되고 있다. 삭제 버튼을 누르면 Category 컴포넌트 안에 PostItem 컴포넌트의 setState가  호출되면서 categoryTodoList todolist 상태가 변하고, 그래서 Category 컴포넌트가 재 렌더링이 된다. 

- useRef로 담은 timeState값을 console.log로 찍어보니 역시나, 삭제 버튼을 눌렀을 때 다른 컴포넌트에 의해 상태가 변경되니 timeState.current 의 최신값이 화면에 반영되면서 console.log에 또 찍힌다.  

- 즉, timeState.current 값이 변경되어도 직접적으로 컴포넌트가 재 렌더링이 되지않지만, Category 컴포넌트 내의 다른 상태가 변경되어 컴포넌트가 리렌더링되는 경우에는 console.log가 실행된다.

- 결론은, useRef의 값 변경은 컴포넌트의 리렌더링을 일으키지 않지만, 컴포넌트가 다른 이유로 리렌더링 되는 경우에는 useRef의 현재값을 콘솔에 출력할 수 있다. 

 

2. 결론

- 이 상황에서는 useState를 쓰든 useRef를 쓰든 렌더링 측면에서는 별 차이가 없다. 어차피 다른 컴포넌트에 의해 리렌더링 되기 때문이다. 
- 하지만, timeState 의 state 값이 h1태그로서 아침 -> morning 으로 글자가 1번만 값이 설정이 되고 그 이후에 상태가 변경될 일이 없을 뿐더러, .current 속성이 변경되더라도 리렌더링 될 필요가 없기 때문에 우리는 useRef 로 처리했다. 
- 즉, useState는 timeState가 변경될 때마다 렌더링이 되고, useRef 는 timeState 값이 변경이 되도 리렌더링이 발생하지 않는다. useRef 는 리렌더링이 되어도 변수의 값이 초기화 되지 않고 변수의 값을 유지한다. 
- useState는 상태 변경이 UI 에 바로바로 반영 되어야할 때 사용하면 좋고, useRef는 상태 변경이 UI 에 바로 반영될 필요가 없거나, 재렌더링 없이 값을 유지해야할 때 쓴다.