State에 너무 많은 걸 부여하지마세요.

응애 나 프론트애긔👶·2024년 5월 26일
0
post-thumbnail

State에 너무 많은 걸 부여하지마세요.

내가 Input을 관리하는법

회사 프로젝트에는 사용자들이 입력하는 input이 정말 많습니다.
프로젝트 특성상 입력 해야할 내용들이 많기 때문에 input을 최대 8개까지도 사용을 했습니다.

하나의 Input에 하나의 State를 가지게 된다면 컴포넌트에서 사용하는 State가 너무 많다고 생각이 들었습니다.

그래서 저는 성격이 같은 input들을 모두 한 State에 담기로 하였습니다.


...

const [filters, setFilters] = useState<Filter[]>(
	[
      {
      	name: "filter1",
        label: "검색조건1",
        type: "input",
        value: ""
      },
      {
      	name: "filter2",
        label: "검색조건2",
        type: "autoComplete",
        items: [1, 2, 3],
        value: ""
      },
      {
      	name: "filter3",
        label: "검색조건3",
        type: "selct",
        items: ["조건1", "조건2", "조건3"],
        value: ""
      }
    ]
)

...

위와 같은 형태로 state를 관리하여 name은 해당 state의 고유한 이름, label은 input 상단 혹은 좌측에 위치하는 라밸, type은 MUI에서 제공하는 단순 input인지, autoComplete인지, select인지를 선택할 수 있는 값, items는 select 형식에 들어가는 option 값, value는 해당 input의 value로 사용하였습니다.

위의 형태로 state를 만들고 onChange 이벤트 또한 하나의 State에서 반복문을 돌아 name을 기준으로 value 값을 변경하도록 하였습니다.

특히 조회 조건에서 활용도와 만족도가 높았으며 이미 만들어놓은 공용 컴포넌트에서 type에 따라 원하는 input 컴포넌트가 생성되니 생산성도 높았습니다.

또한 input(MUI 컴포넌트)의 속성을 확장성 높게 활용할 수 있었고 input을 렌더링하는 컴포넌트에선 원하는 커스텀을 할 수 있다는 강점이 있었습니다.

예를 들어 특정 input을 disabled하고 싶다면 state 객체 안에 disabled를 true를 부여하고 공용 컴포넌트내에서 disabled가 true라면 해당 input은 그 효과를 받도록 할 수 있었습니다.


// 검색조건1을 disabled하고 싶다면
...

const [filters, setFilters] = useState<Filter[]>(
	[
      {
      	name: "filter1",
        label: "검색조건1",
        type: "input",
        disabled: true,
        value: ""
      },
      {
      	name: "filter2",
        label: "검색조건2",
        type: "autoComplete",
        items: [1, 2, 3],
        value: ""
      },
      {
      	name: "filter3",
        label: "검색조건3",
        type: "selct",
        items: ["조건1", "조건2", "조건3"],
        value: ""
      }
    ]
)

...

욕심이 불러낸 잘못된 생각

개발을 이어가던 중 Input 옆에 조회 버튼을 넣어달라는 요구사항을 받았습니다.

이번 페이지도 input의 수가 꽤나 많았고 type 또한 다양해서 만들어 놓았던 공용 컴포넌트와 함께 State를 관리를 이전처럼 하면 되겠다고 생각을 했습니다.

그런데 특정 Input 옆에 버튼을 넣는거까지는 State 객체 안에 handle 함수를 넣어 그 함수가 있다면 버튼을 추가하고 해당 함수를 useEffect를 통해 handler 함수에 넣을 수 있도록 추가했습니다.


// handler 함수 추가

const [filters, setFilters] = useState<Filter[]>(
	[
		...
      {
      	name: "filter2",
        label: "검색조건2",
        type: "input",
        value: ""
      },
		...
    ]
)
      
const handleChangeValue = () => {
  // onChange함수로 Value를 바꾸는 함수
}
      
const handleClickButton = () => {
 // 버튼을 눌렀을 때... 
}

useEffect(()=>{
  setFilters(prev => prev.map(filter => {
    // 검색조건2 일 때 handler 추가
  	if(filter.name === "filter2"){
      const newFilter = {...filter}
      newFilter.onClickFunc = handleClickButton
      return newFilter
    }
    return filter
  }))
},[])

위와 같이 useEffect를 사용하여 검색조건2에 handleClickButton 함수를 넣어줬습니다.

공용 컴포넌트에서는 객체 안에 onClickFunc가 있다면 버튼을 생성하고 해당 버튼에 handleClickButton 함수를 넣어주는 형식으로 설계를 했습니다.

여기서 문제가 발생합니다. 바로 handleClickButton 함수 안에서 바깥의 State의 변화를 감지할 수 없다는 것입니다.

    
const handleClickButton = () => {
  // 검색조건 1의 value 값을 가져오려함
 const filter1Obj = filters.filter(filter => filter.name === "filter1");
 const filter1Value = filter1Obj.value;
 console.log(filter1Value); // ""
 }

위와 같이 handleClickButton 함수 안에서 바깥 state의 값을 가지고 오고 싶은데 State들이 분명 onChange 함수로 값이 변경되었는데도 value 값은 모두 초기값이였습니다.

이유는 React의 State의 특성에 있었습니다.

스냅샷으로 상태를 관리한다는 것을 놓친 상태로 일을 벌려버렸습니다.


급한불부터 끄자

Flash Point는 제가 좋아하는 보드게임입니다

원인을 알았으니 문제를 해결할 차례입니다.

문제는 스냅샷이 함수 내에서 State가 변경되기 이전으로 찍혀있기 때문에 State는 항상 초기값일것입니다.
그렇다면 우리는 State의 변경이 일어날 때마다 함수를 갱신하는 것입니다.


// handler 함수 추가

const [filters, setFilters] = useState<Filter[]>(
	[
		...
      {
      	name: "filter2",
        label: "검색조건2",
        type: "input",
        value: ""
      },
		...
    ]
)
const [observationToggle, setObservationToggle] = useState<boolean>(false);      
const handleChangeValue = () => {
  // onChange함수로 Value를 바꾸는 함수
 // State가 변경될 때를 감지하여 토글을 변경해준다.
  setObservationToggle(prev => !prev);
}
      
const handleClickButton = () => {
 // 버튼을 눌렀을 때... 
}

useEffect(()=>{
  setFilters(prev => prev.map(filter => {
    // 검색조건2 일 때 handler 추가
  	if(filter.name === "filter2"){
      const newFilter = {...filter}
      newFilter.onClickFunc = handleClickButton
      return newFilter
    }
    return filter
  }))
},[observationToggle])

위의 코드와 같이 변경할 때마다 ToggleState를 변경해주고 그것을 handleClickButton 함수를 State에 넣는 useEffect의 의존성배열로 넣는것입니다.

하지만 이러한 방법은 저어어어어어어엉말 좋지 않은 방법입니다. 따라하지마세요.


왜 나쁘다는거지 ?

useEffect로 넣은 handle함수는 함수 내부에 있는 State들은 모두 useEffect의 의존성배열로 넣어야합니다. 그렇지 않다면 스냅샷을 찍을 수 없기에 원하는 동작이 될 수 없습니다.
너무 많은 의존성 배열은 추후 유지보수가 어려워지고 원하지 않는 동작이 일어날 가능성이 높아집니다.

또한 State에 부담이 커집니다. 예시코드는 onClick 함수만 넣었지만 만약 onChange, onKeyDown 등 다양한 handler 함수들을 넣는다면 Input에 단순 Value만 가지도록하려는 원래 의도에서 완전히 벗어나집니다.

물론 이러한 방법으로 개발을 하실 수 있습니다만 경험상 useEffect내에 setState의 코드가 길어지고 앞서 말했듯 유지보수가 어려워질 수 있습니다.
코드의 정답은 없다 생각합니다. 하지만 이렇게 쓰지마세요 ... Please...

마지막으로...

맨 처음 검색조건으로서의 State 형태는 꽤나 괜찮다 생각이듭니다.
type에 따른 Input 형태와 각 Input의 Value를 관리하고 label로 상단의 라밸을 관리하는 꽤나 안정적이고 편리한 공통 컴포넌트였습니다.

하지만 만들어놓은 공용 컴포넌트가 조금 더 많은 기능을 하고 싶었으면 하는 마음이 프랑켄슈타인을 만들었습니다.
객체지향을 공부할 때도 단일책임의 원칙을 배웠음에도 그러지 못한 부분이 아쉬웠습니다.

리팩토링을 할 시간이 없어 위의 코드는 일단 납두려합니다. 동작에는 문제가 없으니 수정을 하다가 잘되던 기능이 안되어버리면 더 큰 문제가 되어버리니 추후 프로젝트 리팩토링을 할 기회가 생긴다면 꼭 수정하려합니다.

긴 글 읽어주셔서 감사합니다.

Reference

https://react.dev/learn/state-as-a-snapshot


0개의 댓글