React를 사용하여 많은 코드를 생성했다면, 상태를 부적절하게 사용했을 가능성이 높습니다. React 개발자들 사이에서 널리 퍼져 있는 오류는 스테이트가 필요한 개별 컴포넌트에 국한되지 않고 전체 애플리케이션 내에서 전역적으로 유지되는 것입니다.

로컬 상태를 통합하기 위해 코드를 리팩토링하는 것은 소프트웨어 개발 모범 사례의 일부로 고려해야 하는 중요한 기술이며, 코드베이스의 성능과 유지보수성을 개선할 수 있습니다.

React의 상태 기본 예시

이 간단한 카운터 애플리케이션에서 React에서 상태를 관리하는 방법에 대한 최소한의 예시를 볼 수 있습니다. 사용자 인터페이스 구축에 널리 사용되는 이 자바스크립트 라이브러리에서 상태를 처리할 때 취하는 일반적인 접근 방식을 보여줍니다.

import {useState} from 'react'
import {Counter} from 'counter'

function App(){
  const [count, setCount] = useState(0)
  return <Counter count={count} setCount={setCount} />
}

export default App

`useState()` 훅의 활용은 `Counter` 컴포넌트의 통합과 함께 1줄과 2줄에서 확인할 수 있습니다. 네이티브 ‘카운트’ 상태와 해당 상태를 수정하기 위한 ‘setCount’ 함수는 주어진 범위 내에서 설정되고 설명됩니다. 그 후, 앞서 언급한 요소들은 추가 실행을 위해 앞서 언급한 `Counter` 컴포넌트로 전달됩니다.

카운터 컴포넌트는 카운트의 현재 값을 표시하는 역할을 하며, 페이지에 카운트를 렌더링하여 이를 수행합니다. 또한 이 컴포넌트에는 카운트 값을 한 단위씩 늘리거나 줄이는 데 사용할 수 있는 ‘setCount’라는 메서드가 있습니다.

function Counter({count, setCount}) {
  return (
    <div>
      <button onClick={() => setCount(prev => prev - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(prev => prev + 1)}>+</button>
    </div>
  )
}

카운터 컴포넌트는 범위 내에서 카운트 변수와 setCount 함수에 대한 로컬 정의를 제공하지 않고 대신 상위 컴포넌트(App)에서 가져왔습니다. 결과적으로 글로벌 상태를 활용하고 있습니다.

전역 상태의 문제점

전역 상태 활용과 관련된 한 가지 잠재적 어려움은 해당 상태를 부모 컴포넌트 내에 저장한 다음 해당 상태를 사용하기 위해 필요한 컴포넌트로 프롭으로 전송하는 것입니다.

여러 컴포넌트에서 상태를 활용하는 경우 공유 범위 내에서 유지하는 것이 적절할 수 있습니다. 그러나 이 특정 사례에서는 카운트 상태에 액세스해야 하는 컴포넌트가 카운터 하나뿐입니다. 따라서 이 정보의 유일한 소비자인 카운터 컴포넌트로 상태 관리를 이전하는 것이 더 효율적이고 논리적입니다.

이 글도 확인해 보세요:  Rust의 제네릭 형식 알아보기

상태를 하위 컴포넌트로 이동

카운터 컴포넌트 내에서 상태를 재배치하면 다음과 같은 모양이 됩니다:

import {useState} from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <button onClick={() => setCount(prev => prev - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(prev => prev + 1)}>+</button>
    </div>
  )
}

애플리케이션의 프레임워크 내에서 하위 요소로 정보를 전달할 필요가 없습니다.

// imports
function App(){
  return <Counter />
}

업데이트된 구현은 이전 버전의 기능을 유지하면서 카운터 컴포넌트 자체에 모든 상태 변수를 통합합니다. 따라서 카운터 컴포넌트의 여러 인스턴스를 서로의 상태 관리를 방해하지 않고 서로 다른 페이지에서 사용할 수 있습니다. 이제 개별 컴포넌트는 자급자족하며 자체 상태 유지 관리를 책임집니다.

보다 복잡한 애플리케이션에서 상태 처리

글로벌 상태를 활용할 때 사용할 수 있는 한 가지 인스턴스는 양식의 컨텍스트 내에 있습니다. 여기에 설명된 App 컴포넌트는 양식 데이터(이메일 및 비밀번호)와 설정자 메서드를 LoginForm 컴포넌트에 전달합니다.

import { useState } from "react";
import { LoginForm } from "./LoginForm";

function App() {
  const [formData, setFormData] = useState({
     email: "",
     password: "",
  });

 function updateFormData(newData) {
    setFormData((prev) => {
      return { ...prev, ...newData };
    });
  }

 function onSubmit() {
    console.log(formData);
  }

 return (
    <LoginForm
      data={formData}
      updateData={updateFormData}
      onSubmit={onSubmit}
    />
  );
}

LoginForm 컴포넌트는 사용자 로그인 세부 정보를 수신하고 표시하도록 설계되었으며, 상위 컴포넌트에서 프로퍼티로 전달되는 updateData 함수를 호출하여 이러한 세부 정보의 제출을 처리할 수 있습니다.

function LoginForm({ onSubmit, data, updateData }) {
  function handleSubmit(e) {
    e.preventDefault();
    onSubmit();
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        type="email"
        id="email"
        value={data.email}
        onChange={(e) => updateData({ email: e.target.value })}
      />
      <label htmlFor="password">Password</label>
      <input
        type="password"
        id="password"
        value={data.password}
        onChange={(e) => updateData({ password: e.target.value })}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

부모 컴포넌트에서 상태를 유지하는 대신, 코드가 활용될 LoginForm.js로 상태를 전송하는 것이 더 적절합니다. 이 접근 방식을 사용하면 다른 컴포넌트(예: 상위 컴포넌트)에 데이터를 의존하지 않는 자급자족 컴포넌트를 만들 수 있습니다. 아래는 LoginForm의 수정된 버전입니다.

import { useRef } from "react";

function LoginForm({ onSubmit }) {
  const emailRef = useRef();
  const passwordRef = useRef();
  
 function handleSubmit(e) {
    e.preventDefault();
    onSubmit({
      email: emailRef.current.value,
      password: passwordRef.current.value,
    });
  }

 return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input type="email" id="email" ref={emailRef} />
      <label htmlFor="password">Password</label>
      <input type="password" id="password" ref={passwordRef} />
      <button type="submit">Submit</button>
    </form>
  );
}

`ref` 속성과 함께 `useRef` React 훅을 사용하면 업데이트 메서드를 수동적으로 전달하는 대신 입력값을 변수에 바인딩할 수 있습니다. 이 방법론은 `useRef` 구현을 통해 작성된 콘텐츠를 간소화하고 폼 효율성을 향상시킵니다.

App.js 파일의 나머지 기능은 로그인 폼 컴포넌트 내에서 트리거되는 onSubmit() 함수 호출에만 관련되어 있습니다. 따라서 유틸리티가 대체되었으므로 추가적인 상태 관리 또는 updateFormData() 메서드가 필요하지 않습니다.

function App() {
  function onSubmit(formData) {
    console.log(formData);
  }

 return (
    <LoginForm
      data={formData}
      updateData={updateFormData}
      onSubmit={onSubmit}
    />
  );
}

앞서 언급한 개발에는 애플리케이션의 상태를 최대한 현지화할 뿐만 아니라 참조를 활용하여 상태에 대한 요구 사항을 없애는 작업도 포함되었습니다. 그 결과 앱 구성 요소의 복잡성이 단일 기능 엔티티로 크게 감소했습니다.

이 글도 확인해 보세요:  HTTP와 HTTPS: 차이점은 무엇인가요?

로그인 폼 컴포넌트의 간소화로 인해 앞서 언급한 두 개의 참조만으로 상태 수정에 신경 쓸 필요가 없어져 더욱 간소화되었습니다.

공유 상태 처리

로컬 상태 관리 전략을 구현할 때 한 가지 잠재적인 문제는 상위 컴포넌트에서 사용하는 상태가 여러 하위 컴포넌트로 전달되는 것이 항상 가능하지 않을 수 있다는 것입니다.

TodoContainer 부모 컴포넌트에는 두 개의 하위 컴포넌트, 즉 TodoList와 TodoCount가 수반됩니다.

function TodoContainer() {
  const [todos, setTodos] = useState([])

  return (
    <>
      <TodoList todos={todos}>
      <TodoCount todos={todos}>
    </>
  )
}

앞서 언급한 두 하위 요소는 모두 상태를 필요로 하며, 따라서 TodoContainer는 해당 상태를 두 요소에 전송합니다. 이러한 경우가 발생하면 상태가 가능한 최소한의 범위로 제한되도록 하는 것이 중요합니다. 앞서 언급한 그림에서 TodosContainer 내에 배치하는 것이 이 로컬리티 원칙의 최적 인스턴스 역할을 합니다.

애플리케이션 구성 요소 내에서 이 조건을 활용하는 것은 정보가 필요한 두 구성 요소의 직계 조상이 아니기 때문에 최적이 아니며, 그 결과 해당 상태의 위치가 더 멀어집니다.

대규모 애플리케이션의 경우 useState() Hook을 단독으로 사용하는 것이 번거로울 수 있습니다. 이러한 경우 상태를 효율적으로 관리하기 위해 React Context API 또는 React Redux를 사용해야 할 수 있습니다.

React Hook에 대해 자세히 알아보기

후크의 활용은 React의 초석 역할을 합니다. React 내에서 후크를 배포하면 일반적으로 클래스를 적용해야 하는 장황한 코드셋을 만들지 않아도 됩니다. 사용 가능한 수많은 훅 중에서 useState() 훅보다 더 보편적인 훅은 없습니다. 다른 주목할 만한 훅으로는 useEffect(), useRef(), useContext()가 있고요.

React를 사용해 애플리케이션을 개발하는 데 능숙해지려면 애플리케이션 내에서 훅을 활용하는 방법을 이해해야 합니다.

By 이지원

상상력이 풍부한 웹 디자이너이자 안드로이드 앱 마니아인 이지원님은 예술적 감각과 기술적 노하우가 독특하게 조화를 이루고 있습니다. 모바일 기술의 방대한 잠재력을 끊임없이 탐구하고, 최적화된 사용자 중심 경험을 제공하기 위해 최선을 다하고 있습니다. 창의적인 비전과 뛰어난 디자인 역량을 바탕으로 All Things N의 잠재 독자가 공감할 수 있는 매력적인 콘텐츠를 제작합니다.