Framework/React & RN

[FE] 프론트엔드에서의 상태관리란 무엇인가? (1) 등장배경과 정의

Joonfluence 2021. 11. 6.

서론

대상독자

  • 프론트엔드(이하 FE) 상태관리를 처음 접하는 웹 프로그래밍 입문자.
  • 큰 맥락에서 FE 상태관리를 이해하고 싶은 웹 프로그래밍 초급자.

오늘의 학습 목표

  • 웹의 역사와 FE 상태관리의 등장배경에 대해서 이해한다.
  • 상태관리의 정의를 이해하고 활용 방법에 대해서 학습한다.
  • 다양한 상태관리 라이브러리들의 장/단점을 비교할 수 있다.
  • 개발 상황에 알맞게, 상태관리 라이브러리를 선택할 수 있다.
  • 실제 웹 어플리케이션에 필요한 기능을 상태관리를 활용하여 구현할 수 있다.

프론트엔드 상태관리란 무엇이며, 왜 필요한가?

FE 개발을 할 때, 가장 중요하게 여겨지는 두 가지가 있습니다. 바로 스타일링과 상태관리인데요, 그래서인지 프론트엔드 개발을 할 때 어떤 상태관리 라이브러리를 사용할 것인지가 화두로 떠오르기도 합니다. 오늘 글을 통해, 상태관리에 대해서 이해하고 상황에 따라 알맞은 도구를 선택할 수 있길 바랍니다.

등장배경

웹의 역사는 백엔드(이하 BE) 서버에서 모든 데이터를 관리하던 단계에서 서버가 기존에 해오던 역할을 줄여나가는 방향으로 발전해왔습니다. 초창기에는 BE 서버에서 브라우저의 요청(주로 URL 입력)에 따라, 웹 페이지를 동적으로 렌더링 해줬습니다. 따라서 FE에서의 상태관리는 물론, FE 서버조차 필요 없었습니다. 그러나 점차 서버의 부하를 줄이고 더 많은 사용자들에게 원할한 서비스를 제공하기 위해, 페이지 렌더링과 UI 작업을 처리하기 위해, 추가로 FE 서버를 두기 시작했습니다. 그럼으로써 현재의 클라이언트 - 서버 구조가 널리 퍼지게 됐고 웹 FE와 웹 BE의 역할이 명확하게 구분되게 됐죠.

본론

정의

그렇다면 본격적으로 상태관리는 State Management를 한국어로 직역한 단어입니다. 쉽게 말해, 데이터를 관리하는 방법을 말하죠. FE에서의 상태관리란 데이터를 설계된 UI, UX에 맞게 설계하고 구현하는 일입니다. 또한 네트워크를 통해 서버로 전달되는 클라이언트의 요청에 따라 변화하는 상태를 관리하는 일입니다.

먼저 제 페이스북 타임라인 화면을 살펴보며, 화면에 나타난 데이터를 살펴보겠습니다.

  • 작성자
  • 작성시간
  • 공개범위
  • 게시글
  • 좋아요 수
  • 댓글

첫째, 데이터를 설계된 UI, UX에 맞게 설계하고 구현하는 일

이는 서버로부터 받아온 데이터를 어플리케이션 화면에 걸맞게 가공해 제공하는 일과 마찬가지입니다. 여러 데이터가 있지만, 작성시간 데이터를 기반으로 설명하겠습니다. 작성시간은 작성시점으로부터 얼마만큼의 시간이 지났는지를 보여줍니다. '22시간'과 같이요. 사실 해당 데이터는 Date 타입 데이터로 'Wed Nov 10 2021 17:51:58 GMT+0900 (한국 표준시)'와 같은 형식으로 나타납니다. 하지만 이와 같은 데이터는 알아보기가 힘듭니다. 따라서 데이터의 형태를 사용자가 보기 쉽게 바꿔주는 것이죠. 그리고 작성시간이 24시간이 지나간 데이터를 '어제', 48시간이 지난 시점에는 11월 8일과 같이 월 데이터와 일 데이터로 보여줍니다.

또한 사용자가 UI를 통해 데이터를 조작할 수 있는 환경을 마련하는 일을 말합니다. 페이스북 타임라인에서 대표적으로 관리해야 할 데이터는 Post(게시글) 일 것입니다. Post에는 글 정보 뿐 아니라, 사용자 공개범위 정보, 위치 그리고 사용자 기분 등을 UI를 통해 변경할 수 있도록 설계 되어 있습니다. 프론트엔드에선 사용자 데이터가 서버로 전송되기 전까지, 계속 데이터들을 관리합니다. 이 역시, 상태관리의 영역입니다.

네트워크를 통해 서버로 전달되는 클라이언트의 요청에 따라 변화하는 상태를 관리하는 일

사용자의 눈에는 보이지 않지만 항상 생각해야 할 상태관리가 있습니다. 바로 네트워크 요청에 대한 상태관리입니다. 사용자 데이터는 결국, 서버에서 변경되게 됩니다. 이는 네트워크를 통해 전송됩니다. 이 요청은 성공할 수도 있지만, 실패할 수도 있습니다. 따라서 다음과 같은 경우로 분류될 것 입니다.

1) 요청 실패 (Request Failure)
2) 요청 중 (Loading)
3) 요청 성공 (Request Success)

성공했다면 사용자에게 작은 팝업을 띄워줄 수 있고, 실패했다면 에러 메세지를 사용자에게 보여줄 수도 있겠죠. 대표적인 경우가 사용자 로그인입니다. 사용자가 존재하지 않는 아이디를 입력했을 때, 로그인이 되어선 안될 것입니다. 에러가 발생한 이유를 사용자에게 알림으로써, 사용자는 자신의 행동을 수정할 수 있겠죠.

리액트에서의 상태관리 (Get-Post)

그렇다면 본격적으로 실습에 들어가겠습니다. 백엔드는 json-server를 사용하고 프론트엔드는 creact-react-app를 활용해, 간단한 To Do App을 만들어보겠습니다. 참고로 json-server는 REST API 기반으로, 지금과 같이 간단한 CRUD 동작을 테스트해보기에 좋은 라이브러리입니다. 물론 프로덕션 환경에선 사용할 수 없습니다. creact-react-app 역시, 리액트 환경에서 빠르게 셋업을 도와주는 좋은 툴입니다. 자, 그러면 본격적으로 네트워크 요청에 따른 상태관리를 실제 코드와 함께 진행해보도록 하겠습니다. 먼저, 리액트 환경설정을 해두겠습니다. 또 로딩 처리를 위한 로더블 라이브러리인 react-spinners도 설치해줍니다.

mkdir simple-todoApp 
cd simple-todoApp 
npx create-react-app client 
yarn add react-spinners 

간단하게 화면 구성을 먼저 진행해보죠. 간단한 코딩이고 상태관리를 설명하기 위함이 주목적이므로, 세부적인 코드들에 대한 설명은 생략하겠습니다.

(코드펜 화면)

자, 그러면 목업 서버로부터 데이터를 받아오기 위해 json-server를 사용해보겠습니다.

mkdir server 
cd server 
npm install -g json-server 
touch db.json 

db.json은 서버 요청에 따른 데이터가 저장되는 파일입니다. 그리고 간단하게 id, title, description을 가진 형태로 작성하면 될 것 입니다. 해당 항목들을 배열의 형태로 작성해줍니다. 이것으로 준비는 모두 끝입니다. 쉽죠? 이제 json-server를 실행해, 실제 요청을 처리해줄 수 있도록 만들어줄 것입니다.

json-server --watch db.json --port 3004 

자, 서버는 모두 준비가 끝이 났습니다. 그럼 아제 client에서 서버로부터 데이터를 요청하는 일만 남았겠죠?

기본 로직은 위와 같습니다. 성공적으로 요청을 처리하면 data를 전송하고 그렇지 않을 경우에는 undefined를 보냅니다. 또한 요청 성공, 요청 중, 요청 실패를 구분하기 위해, isLoading과 isError란 변수를 추가로 반환해줄 것입니다. 이로써 3단계로 구분할 수 있을 것입니다. 요청 성공 시에는 loading과 error 변수를 false로, 요청 중에는 loading 변수를 true로, 요청 실패 시에는 data를 undefined로 보내줍니다.

export const GetTodosAPI = () => {
  let isApiLoading = true;
  let isApiError = false;
  let data = undefined;

  return fetch("http://localhost:3001/todos")
    .then((res) => {
      if (res.ok) {
        isApiLoading = false;
        isApiError = false;
        data = res.json();
      } else {
        throw new Error("unexpected Error Occured!!");
      }
      return [isApiLoading, isApiError, data];
    })
    .catch((err) => {
      console.log(err);
      isApiError = true;
      isApiLoading = false;
      return [isApiLoading, isApiError, data];
    });
};

그러면 실제 화면에서 확인해볼까요? 초기 렌더링 시에, 해당 동작들을 처리하기 위해 useEffect 훅에 로딩/에러 처리에 대한 부분들을 추가했습니다. 자세한 내용은 리액트 공식문서 설명을 참고 바랍니다.

  const [isLoading, setIsLoading] = useState(false);
  const [isTodoLoading, setIsTodoLoading] = useState(false);
  const [isError, setisError] = useState(false);
  const [todos, setTodos] = useState([]);

  const GetTodos = useCallback(async () => {
    const [isApiLoading, isApiError, data] = await GetTodosAPI();
    const returnData = await data;

    if (isApiLoading) {
      setIsTodoLoading(true);
    }

    if (isApiError) {
      setisError(true);
    }

    if (returnData) {
      setTodos(returnData);
    }
  }, []);

  useEffect(() => {
    GetTodos();
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        <nav className="App-nav">
          <img src={logo} className="App-nav--logo" alt="logo" />
          <h1 className="App-nav--title">Sample To Do App</h1>
          <ul>
            <li>
              <img
                src={user}
                className="App-nav--profileImg"
                alt="user profile"
              />
            </li>
            <li>
              <span>UserName</span>
            </li>
          </ul>
        </nav>
      </header>
      <main className="App-main">
        <section className="main-input">
          <input className="main-input--input"></input>
          <Button isLoading={isLoading} />
        </section>
        <section className="main-todo">
          <ul className="main-todos">
            {isTodoLoading ? (
              <div className="main-todos--loading">
                <ClipLoader loading={isTodoLoading} size={40} />
              </div>
            ) : (
              todos &&
              todos.length > 0 &&
              todos.map((item) => (
                <li className="main-todos--list">
                  <span>{item.id}</span>
                  <span>{item.title}</span>
                  <div>{item.done ? "done" : "on going"}</div>
                </li>
              ))
            )}
            {isError && <div className="main-todos--error">Error Occured</div>}
          </ul>
        </section>
      </main>
    </div>
  );
};

export default App;

요청과 응답 메커니즘

그리고 이와 같은 과정을 그림으로 표현해보면, 아래와 같을 것입니다. 지금은 db.json에 저장된 데이터를 조회하고 추가하는 형식입니다만, 실제로는 DB에 저장될 것이며 서버에서 DB로 접근하여 클라이언트의 Get / Post 요청을 처리할 수 있는 로직이 존재할 것입니다.

결론

이것으로 프론트엔드에서의 네트워크 상태관리에 관하여, 살펴보았습니다. 또한 전체적인 맥락에서의 상태관리는 어떠한 것인지에 관해서도 짧게 살펴보았습니다. 읽어주셔서 감사드립니다.

반응형

댓글