공식문서/리액트

어떻게 스테이트를 잘 구성하는가? :스테이트 구성에 대한 원칙

Integer Essence 2023. 2. 23. 16:15

 

 

 

나는 그동안 스테이트 관리를 어떻게 해왔는가?  를 돌아보며 

 

중급자용이라고 떡 박혀있는 공식문서의 리액트 스테이트 관리에 대한 글을 정리해보고자한다. 

 

 

 

우리의 앱이 커질수록 의도적으로 컴포넌트 사이의 데이터흐름과 어떻게  state가 구성되는지 생각하는게 도움이된다.

 

 불필요하고 중복되는 스테이트는 일반적인 버그 소스다.

 

 

그러면 어떻게 스테이트를 잘 구조화할수있을까?  그 전에 

 

#상태를 잘 구조화하는것의 이점은 무엇일까?

 

상태를 잘 구조화 한다면 

 

디버그와 수정이 쉬운 컴포넌트와 끊임없는 버그를 만드는 컴포넌트의 차이를 만든다. 

 

스테이트에서 중요한 원칙은 불필요하고 중복되는 정보가 포함되면 안된다는 것 

 

 

 

#스테이트 구성에 대한 원칙 

여러가지 스테이트들이 있는 컴포넌트를 작성할때 

 

우리는 기본적으로 

 

1. 얼마나 많은 스테이트 변수 상태값이 있냐? 

 

2. 어떤 데이터 형태를 취할것이냐 

 

이 두가지를 정해야 되며 

 

리액트 공식문서는 상황에따라 스테이트 구성에 대해 더 나은 선택을 할 수있는 몇 가지 가이드를 제시한다.

 

1) 스테이트들을 그룹으로 연관지어라 

 

항상 2개 혹은 그이상의 state 변수들을 같은 시간에 업데이트한다면 하나의 스테이트로 합칠것을 고려해라. 

 

 

ex) x좌표 y좌표가 따로있다면 객체로 합친다.

 

x,setX   ,0  / y, setY  ,0 이 아닌 position , setPosition  = useState({x:0,y:0})로 

 

이렇게 해놓으면 항상 동기화 된 상태를 유지하는것을 잊지않을것임. (스테이트 두개가 같은 시간에 바뀔때의 경우ㅇ)

 

 

 

 

 

 

2)상태의 모순을 피해라

서로 모순되는 방식으로 그리고 각각 이 동의하지않는 형태로 스테이트가 구성될때 , 실수의 여지가 남게된다. 이것을 피해라 

 

서로 모순되는 방식이랑 각각이 동의하지않는 형태가 뭔 소리

 

 

 

 

ex) 예시를 보면 이해가된다. 

  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

 

통신할때 다음 과 같은 코드가있다면 시작에 sending state는 true 로 바뀌고 끝날때 false 

setIsSent 는 true 같은 형태로 바뀜 

 

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

이 코드는 제대로 동작되지 않을수 있다는 가능성이 열려있다. 

 

setIsSent나 setIsSending을 호출하는걸 까먹는다면 

 

isSending이랑 isSent가 둘다 동시간에 똑같이 true 가 될수있다.  컴포넌트가 더 복잡하면 복잡할수록 뭔 일이 일어났는지 이해하기 어려워질것임

 

보내는 도중에 대한 상태코드랑 보내고 난뒤의 상태코드가 동시에 true이면 안됨으로 

 

status라는 state로 변경하고 상태에 대한 값을 typing(입력중) , sending(보내는중) , sent(보냄) 세가지로 나눠서 쓸것을 제시함 

 

 const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }
  
  
  const isSending = status === 'sending';
  const isSent = status === 'sent';

 

 

 

 

 

 

 

 

 

 

 

 

3)불필요한 상태를 피해라 

props로 받아온 상태값이나  이미 존재하는 스테이트 변수 들이 랜더링 되는동안 몇몇 정보를 계산할수있는데 , 그것을 컴포넌트 스테이트 값에 넣으면 안된다. 

 

ex)

 

 const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

 

딱봐도 풀네임이 불필요하다는걸 알수있다.

 

 

  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

 

 

 

props에 대한 예시 

 

function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);

 

이렇게하면 부모에서 다른 messageColor 를 전달할 경우 컬러 스테이트는 업데이트 되지않는다.

태는 첫 번째 렌더링 중에만 초기화되니까 

 

 

 

 

4) 중복된 상태를 피해라

같은 데이터가 여러 스테이트 변수나 , 중첩된 객체 사이 에서  반복되는 경우 동기화를 유지하기가 굉장히 어렵다.

가능하면 중복을 줄여라

 

 

ex)

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

 

 

items와 selectedItem 은 중복이다.  items에 있는것을 그대로 배껴오니까 

 

이게 문제인 이유? 

 

 

 

choose를 먼저 클릭하고 여행간식 아이템 input을 바꾸면 변동 사항이 하단에 적용되지않는다. 

 

input을 먼저 바꾸고 choose를 선택해야 바뀐게 하단에 표시됨 

 

옆에 코드를 보면 알겠지만 selectedItem을 업데이트하는것이 없기도하고 중복이기도 해서 그렇다. 

 

 

 

 

 

 

같은 오브젝트에서 두개의 위치를 사용하는 중복 스테이트임으로 해당 부분을 다음과같이 수정해준다. 

 

 

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

 

 

 

 

 

이렇게 바꾸면 choose를 선택하고 나서 input의 내용을 바꾸면 바뀌는대로 하단에 내용도 같이 변화한다. 

 

필수적으로 필요한 건 id 값이다. 해당 배열의 객체 전체가 아니라 

 

고로 id만 살림

 

input에 handleItemChange  method 에 seItems 가 변할때마다 어짜피 렌더링 됨으로 아래에도 정보가 변화하게됨

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

5)깊이 중첩된 상태를 피해라 

깊은 계층적 구조를 가진 스테이트는 업데이트하기 힘들다. 가능하면 평평한 상태로 구조화시키는것을 선호.

 

 

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kenya',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Morocco',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigeria',
        childPlaces: []
      }, {
        id: 9,
        title: 'South Africa',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Americas',
      childPlaces: [{
        id: 11,
        title: 'Argentina',
        childPlaces: []
      }, {
        id: 12,
        title: 'Brazil',
        childPlaces: []
      }, {
        id: 13,
        title: 'Barbados',
        childPlaces: []
      }, {
        id: 14,
        title: 'Canada',
        childPlaces: []
      }, {
        id: 15,
        title: 'Jamaica',
        childPlaces: []
      }, {
        id: 16,
        title: 'Mexico',
        childPlaces: []
      }, {
        id: 17,
        title: 'Trinidad and Tobago',
        childPlaces: []
      }, {
        id: 18,
        title: 'Venezuela',
        childPlaces: []
      }]
    }, {
      id: 19,
      title: 'Asia',
      childPlaces: [{
        id: 20,
        title: 'China',
        childPlaces: []
      }, {
        id: 21,
        title: 'Hong Kong',
        childPlaces: []
      }, {
        id: 22,
        title: 'India',
        childPlaces: []
      }, {
        id: 23,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 24,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 25,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 26,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 27,
      title: 'Europe',
      childPlaces: [{
        id: 28,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 29,
        title: 'France',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 34,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 35,
      title: 'Oceania',
      childPlaces: [{
        id: 36,
        title: 'Australia',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 40,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 41,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 42,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 43,
    title: 'Moon',
    childPlaces: [{
      id: 44,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 45,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 46,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 47,
    title: 'Mars',
    childPlaces: [{
      id: 48,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 49,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

 

 

아.... 뭐어떻게 immer 같은거를 쓰라고하려나 ? 하고 생각했는데 (딥 다이브의 메모리 사용량 향상 시키기 관련에 immer 가 나오긴한다 :https://beta.reactjs.org/learn/choosing-the-state-structure#improving-memory-usage)

 

공식문서는 역시 생각하는게 달랐다 

 

이걸 다음과 같이 평평하게 만들수 있다. 

 

 

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 43, 47],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 27, 35]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  3: {
    id: 3,
    title: 'Botswana',
    childIds: []
  },
  4: {
    id: 4,
    title: 'Egypt',
    childIds: []
  },
  5: {
    id: 5,
    title: 'Kenya',
    childIds: []
  },
  6: {
    id: 6,
    title: 'Madagascar',
    childIds: []
  }, 
  7: {
    id: 7,
    title: 'Morocco',
    childIds: []
  },
  8: {
    id: 8,
    title: 'Nigeria',
    childIds: []
  },
  9: {
    id: 9,
    title: 'South Africa',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Americas',
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brazil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canada',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'Mexico',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trinidad and Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Asia',
    childIds: [20, 21, 22, 23, 24, 25, 26],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'Hong Kong',
    childIds: []
  },
  22: {
    id: 22,
    title: 'India',
    childIds: []
  },
  23: {
    id: 23,
    title: 'Singapore',
    childIds: []
  },
  24: {
    id: 24,
    title: 'South Korea',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Thailand',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Vietnam',
    childIds: []
  },
  27: {
    id: 27,
    title: 'Europe',
    childIds: [28, 29, 30, 31, 32, 33, 34],   
  },
  28: {
    id: 28,
    title: 'Croatia',
    childIds: []
  },
  29: {
    id: 29,
    title: 'France',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Germany',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Italy',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Portugal',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Spain',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Turkey',
    childIds: []
  },
  35: {
    id: 35,
    title: 'Oceania',
    childIds: [36, 37, 38, 39, 40, 41, 42],   
  },
  36: {
    id: 36,
    title: 'Australia',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  39: {
    id: 39,
    title: 'Fiji',
    childIds: []
  },
  40: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  41: {
    id: 41,
    title: 'New Zealand',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Vanuatu',
    childIds: []
  },
  43: {
    id: 43,
    title: 'Moon',
    childIds: [44, 45, 46]
  },
  44: {
    id: 44,
    title: 'Rheita',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Piccolomini',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Tycho',
    childIds: []
  },
  47: {
    id: 47,
    title: 'Mars',
    childIds: [48, 49]
  },
  48: {
    id: 48,
    title: 'Corn Town',
    childIds: []
  },
  49: {
    id: 49,
    title: 'Green Hill',
    childIds: []
  }
};

 

 

그밖에 챌린지 요소들도 있으니 공식문서에 가서 해보면 좋을듯 하다 다 마저 할려다가 하루에 약 2~40분 가량 읽으면서 작성하는데 넘 글이 길어지고있어서 핵심 내용이라고 생각되는 부분만 가지고왔다. 

 

 

 

 

 

 

 

 

해당 공식문서 : 

https://beta.reactjs.org/learn/choosing-the-state-structure