Post

리액트 state 구조

✨리액트 state

컴포넌트의 데이터(상태)
함수형 컴포넌트에서는 useState를 사용하여 상태를 관리한다.

📘useState()

1
2
3
import { useState } from "react";

const [state, setState] = useState();
  • state: 렌더링 간에 데이터를 유지하기 위한 state 변수
  • setState: 변수를 업데이트하고, react가 컴포넌트를 재렌더링하도록 만드는 state setter 함수

📘state 특징

1. 여러 개의 state를 지정할 수 있음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState } from "react";
import data from "./data.js";

function App() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }
  return (
    <div>
      <button onClick={handleNextClick}>Next</button>
      <h2>data[index]</h2>
    </div>
  );
}

2. state는 격리된다.

동일한 컴포넌트를 한번이상 렌더링한다면 각 복사본은 완전히 독립적인 state를 가진다. 한 개의 컴포넌트의 state를 수정해도, 다른 컴포넌트에는 영향을 미치지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Gallery from "./Gallery.js";

function Page() {
  return (
    <div className="Page">
      <Gallery />
      <Gallery />
    </div>
  );
}

function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }
  return (
    <div>
      <button onClick={handleNextClick}>Next</button>
      <h2>data[index]</h2>
    </div>
  );
}
  • 첫번째 Gallery 컴포넌트의 버튼을 눌러도, 두번째 Gallery 컴포넌트는 다음 state를 보여주지 않음

📘state 구조화 원칙

1. 연관된 state 그룹화하기

두 개 이상의 state 변수가 항상 동시에 업데이트 된다면, 하나의 state로 관리하는 것이 좋음

마우스 커서를 움직이면 좌표가 업데이트되는 상태를 관리할 때에는 x, y각각의 state로 관리하는 것보다는 하나의 state에서 관리하는 것이 좋다

1
2
3
// 각각 state로 관리
const [x, setX] = useState(0);
const [y, setY] = useState(0);
1
2
// 하나의 state로 관리
const [position, setPosition] = useState({ x: 0, y: 0 });

2. State의 모순 피하기

단일 소스 진실(Single Source of Truth)
여러개의 state들이 모순되고, 불일치되는 방식으로 state를 구성하면 안된다. 하나의 상태를 여러 하위 컴포넌트에 공유함으로써 모든 상태가 일관되게 유지해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { useState } from "react";

function FeedbackForm() {
  const [userData, setUserData] = useState({
    username: "",
    email: "",
    age: 0
  });

  const handleUsernameChange = (event) => {
    setUserData({ ...userData, username: event.target.value });
  };

  const handleEmailChange = (event) => {
    setUserData({ ...userData, email: event.target.value });
  };

  const handleAgeChange = (event) => {
    setUserData({ ...userData, age: event.target.value });
  };

  return (
    <div>
      <h1>User Information</h1>
      <form>
        <Input
          inputType="text"
          labelText="Username"
          value={userData.username}
          onChange={handleUsernameChange}
        />
        <br />
        <Input
          inputType="email"
          labelText="Email"
          value={userData.email}
          onChange={handleEmailChange}
        />
        <br />
        <Input
          inputType="number"
          labelText="Age"
          value={userData.age}
          onChange={handleAgeChange}
        />
      </form>
      <div>
        <h2>Preview:</h2>
        <p>Username: {userData.username}</p>
        <p>Email: {userData.email}</p>
        <p>Age: {userData.age}</p>
      </div>
    </div>
  );
}

function Input({ inputType, labelText, value, onChange }) {
  return (
    <label>
      {labelText}:
      <input type={inputType} value={value} onChange={onChange} />
    </label>
  );
}

3. 불필요한 state 피하기

  • 필요한 상태만 관리하기: 상태관리는 최소한으로 유지해야 함
  • 계산 가능한 데이터는 상태로 관리하지 않기: 컴포넌트 렌더링시에 필요한 데이터를 계산할 수 있다면 상태로 관리하는것보다는, 필요할 때마다 계산하는 것이 더 나음
  • 부모 컴포넌트 상태 끌어올리기: 여러 컴포넌트가 동일한 데이터에 접근하는 경우에는 부모 컴포넌트에서 상태관리하는 것이 나음
  • 컴포넌트 간의 상태 동기화 최소화하기

아래의 코드는 이름(firstName), 성(lastName), 이름 + 성(fullName) 3가지의 상태를 관리한다. 이 중 fullName은 firstName과 lastName으로 연산할 수 있기 때문에 불필요한 state이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { useState } from "react";

function Name() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [fullName, setFullName] = useState("");

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + " " + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + " " + e.target.value);
  }
  return (
    <div>
      <label>
        First name: <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name: <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <h4>FullName is {fullName}</h4>
    </div>
  );
}

firstName과 lastName을 사용해서 fullName을 구해 사용하는 방법으로 수정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useState } from "react";

function Name() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  const handleFullName = () => {
    return `${firstName} ${lastName}`;
  };

  return (
    <div>
      <label>
        First name: <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name: <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <h4>FullName is {handleFullName()}</h4>
    </div>
  );
}

4. State의 중복 피하기

상태의 중복이란 동일한 데이터를 여러 컴포넌트에서 유지하는 것을 의미하고,상태가 중복일 경우 데이터의 불일치나 불필요한 복잡성이 발생할 수도 있다.

  • 단일 소스로 상태 관리: 상태 관리를 한 곳에서 하고 필요한 컴포넌트에서 사용 (redux,jotai 등 상태관리 라이브러리 사용)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { useState } from "react";

const initialItems = [
  { title: "pretzels", id: 0 },
  { title: "crispy seaweed", id: 1 },
  { title: "granola bar", id: 2 }
];
function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(items[0]);

  const handleItemChange = (id, e) => {
    setItems(
      items.map((item) =>
        item.id === id ? { ...item, title: e.target.value } : item
      )
    );
  };
  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={(e) => {
                handleItemChange(item.id, e);
              }}
            />{" "}
            <button
              onClick={() => {
                setSelectedItem(item);
              }}
            >
              Choose
            </button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

위의 코드는

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

로 state가 중복되어있습니다.

selectedItem에 해당 item의 모든 내용({id: 0, title: 'pretzels'})을 담는 것이 아닌, 해당 item의 id만 담아 필요한 state를 계산해서 사용할 수 있습니다.

  • items = [{ id: 0, title: ‘pretzels’}, …]
  • setSelectedId = 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { useState } from "react";

const initialItems = [
  { title: "pretzels", id: 0 },
  { title: "crispy seaweed", id: 1 },
  { title: "granola bar", id: 2 }
];
function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  // 선택한 아이템 계산
  const selectedItem = items.find((item) => item.id === selectedId);

  const handleItemChange = (id, e) => {
    setItems(
      items.map((item) =>
        item.id === id ? { ...item, title: e.target.value } : item
      )
    );
  };
  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={(e) => {
                handleItemChange(item.id, e);
              }}
            />{" "}
            <button
              onClick={() => {
                setSelectedItem(item.id);
              }}
            >
              Choose
            </button>
          </li>
        ))}
      </ul>
      <p>You picked {items.title}.</p>
    </>
  );
}

5. 깊게 중첩된 state 피하기

상태의 구조가 복잡하고 중첩되어 있어서 상태를 관리하고 업데이트하기 어렵기때문에 코드의 유지보수성이 떨어지고, 버그가 발생하기 쉽다.

  • 단순한 상태 구조 유지: 깊게 중첩된 상태보다는 평면화된 상태 구조를 사용
  • 상태를 분할하고 컴포넌트로 전달: 깊게 중첩된 상태 대신 상위 컴포넌트에서 상태를 관리하고 하위 컴포넌트로 필요한 데이터를 전달
  • 불변성 유지: 깊에 중첩된 상태에서 하위 객체를 직접적으로 변경하는 것이 아니라 새로운 객체를 생성하여 상태를 업데이트 해야함
  • 컴포넌트 분리: 관리하기 어렵다면, 컴포넌트를 분리하여 상태를 관리 (더 작은 단위의 상태)

참고사이트

This post is licensed under CC BY 4.0 by the author.