개발자일기/리액트 이야기

React로 요리를 찾아주는 기능 구현 (a.k.a 냉장고 털기) - 7 (state)

뫙뭉 2022. 3. 3. 17:48
반응형

State, 변수의 상태관리

리액트에서는 변화하는 값에 대해 클래스형 컴포넌트에서 state를 이용하여 값을 관리하였다.

함수형 컴포넌트에서는 원래 state를 관리할 수 없었기에 state를 사용할 경우 클래스형 컴포넌트를 선택적으로 사용하였는데 이러한 부분을 개선하기 위해 만들어진 기술이 Hooks.

state뿐만 아니라 Life-Cycle-Method 의 사용에도 갖고 있었던 문제점을 리액트 16.8에서 Hooks라는 개념이 도입되면서 useState, useEffect 등으로 손쉽게 함수형 컴포넌트에서도 사용이 가능하게 되었다.

 

state를 이용하는 경우는 매우 다양하다.

간단한 예로 로딩 스피너를 들 수 있는데, 듣기에는 아주 단순해 보이는 이런 경우에도 state로 관리할 수 있다.

API 호출 시 isLoading의 state를 true로 변경하여 스피너를 나타낸 후,

data fetch가 끝나면 isLoading의 state를 false로 변경하여 이에 따라 페이지의 랜더링 여부를 결정하게 되면 로딩 스피너를 나타내는 로직이 완성된다.

 

용도에 대해서 알아보았으니 이제 사용법에 대해서 간단히 알아보자.

 

사용 방법을 알아보자

함수형 컴포넌트에서의 사용법은 기본적으로 Hook을 사용하기 때문에 useState를 import 해 와야한다.

가장 상단에 import { useState } from 'react' 를 적어주면 사용할 준비가 완료된다.

 

state를 사용하기 위해서는 우선 선언을 해야한다.

다음 예시를 살펴보자

 

import { useState } from 'react';

const App = () => {
  const [count, setCount] = useState(0);
  
  function countUp () {
   setCount(count+1);
  }
  
  return (
    <Container countUp={countUp} />
  )

}

 

선언할 때에는 [변수명, set변수명]을 사용한다.

변수명은 알겠는데 'set변수명' 은 뭐지? 라는 생각이 들 것이다.

set변수명이란 말 그대로 변수를 set하는데에 사용하는 함수라고 할 수 있다.

 

const [count, setCount] = useState(0); 

이 문장을 알기 쉽게 풀어서 설명해보자면

'0으로 초기화한 변수 count를 선언하고 나중에 바꿀 때 setCount를 사용해서 변경하겠습니다.' 라는 뜻이 되겠다.

 

state를 하위 컴포넌트에 전달하고 싶은 경우 countUp={countUp} 와 같이 props의 이름, 전달할 props를 적어서 전달해 주면 하위 컴포넌트에서 사용 가능하다.

 

// container.jsx

const Container = ({countUp}) => {  
  return (
    <div onClick={countUp}>
    ...
    </div>
  )
}

또한 하위 컴포넌트에서는 props를 받아서 간단히 사용하면 된다.

원래는 props 자체를 인자로 받아서 사용하나 상위 props의 이름과 전달값이 같을 경우 중괄호 안에 넣어 위와 같이 사용이 가능하다.

 

프로젝트에서 써보기

그렇다면 냉장고 털기에서는 어떻게 사용했는지 몇가지 사례를 예로 들어보겠다.

 

import './App.css';
import Search from './component/Search';
import SelectedItem from './component/SelectedItem';
import Result from './component/Result';
import { useState } from 'react';
import Hero from './component/Hero';

function App({itemsData, naver, youtube}) {

  const [data, setData] = useState(itemsData.items)
  const [filteredItem, setFilteredItem] = useState();
  const [selectedItem, setSelectedItem] = useState([]);
  const [keyword, setKeyword] = useState();  
  const [naverData, setNaverData] = useState([]);
  const [youtubeData, setYoutubeData] = useState();
  const [NLoading, setNLoading] = useState(false);
  const [YLoading, setYLoading] = useState(false);
  
  const connectAPI = () => {
    setNLoading(true);
    setYLoading(true);
    naver
      .search(selectedItem.join(" ") + " 요리")
      .then((res) => {
        setNaverData(res);
      })
      .then((res) => {
        setNLoading(false);
      });
    youtube
      .search(selectedItem.join(" ") + " 요리")
      .then((res) => {
        setYoutubeData(res);
      })
      .then((res) => {
        setYLoading(false);
      });
  }

  const addSelectedItem = (item) => {
    const updated = [...selectedItem, item ]

    if(selectedItem.includes(item)){
      
      const updated2 = updated.filter(selected => selected !== item);
      setSelectedItem(updated2);
      return;
    }
    setSelectedItem(updated)
  }

  const autoComplete = (e) => {
    const currentValue = e.target.value;
    setKeyword(currentValue);

    const filtered = data.filter(item => item.name.includes(currentValue));    
    setFilteredItem(filtered);    
  }

  const resetSelected = () => {
    setSelectedItem([]);
    setYoutubeData();
    setNaverData();
  }

  return (
    <div className="App">
      <Hero />  
      <div className="container">
        <section className="search">
          <Search
            itemsData={itemsData}
            addSelectedItem={addSelectedItem}
            autoComplete={autoComplete}
            filteredItem={filteredItem}          
            resetSelected={resetSelected}
            selectedItem={selectedItem}
            connectAPI={connectAPI}
          />
          <SelectedItem selectedItem={selectedItem}/>
        </section>        
        <Result
        naverData={naverData}
        youtubeData={youtubeData}
        NLoading={NLoading}
        YLoading={YLoading}
      />
      </div>
    </div>
  );
}

export default App;

 

냉장고 털기에서 state를 사용하여 구현한 기능은 다음과 같다.

- 재료 검색 자동완성 기능

- 선택한 재료 보여주기

- API를 통한 data 전달

- data 받아오는 동안의 로딩 스피너 구현

항상 많은 값들을 직접 전달해야 하는 것일까? - 상태관리 라이브러리

내가 다뤘던 프로젝트는 규모가 크지 않기 때문에 일일이 전달해도 코드를 짜는 데에 시간이 오래걸리지도 않을 뿐더러 혼동하는 경우도 거의 없다. 그렇기 때문에 props를 전달하여 사용하여도 큰 문제가 없었다.

하지만 하위 컴포넌트가 100개, 1000개 늘어나게 된다면?

우리는 props를 전달에 전달에 전달에 전달에... 아주 머리가 터져나가게 된다 이말이다.

이런 경우에 사용하라고 만들어 놓은 것들이 바로 '상태 관리 라이브러리' 이다. 리액트를 다뤄본 유저들이라면 한번쯤 들어봤을 Redux가 대표적이며 이외에도 MobX, Recoil 등이 여기에 속한다.

이런 라이브러리 사용의 의의는 중앙에서의 일괄적인 상태관리와 전달받는 것이 아닌 컴포넌트로의 직접적인 state 전달에 있다.

Redux를 접해본 분이라면 많이 봤을 이미지

사실 위에서 설명한 대규모 프로젝트가 아니라면 상태관리 라이브러리는 필수사항이 아니다. 현재 프로젝트의 환경에 따라서 선택적으로 사용하면 된다.

Redux의 사용법에 대해서는 추후에 다뤄보도록 하겠다.

 

마아무리...

state에 관해 간단히 알아보았다. 위에서도 말했지만 state는 변화가 일어나는 거의 모든 변수에서 사용을 하는 요소이다.

어려운 개념이 아니기 때문에 연습을 조금만 한다면 금방 익숙해 질 수 있을 것이다.

반응형