요약

기존의 useState를 사용하는 방식이 성능 문제때문에 setState를 만날때마다 처리하는게 아닌 일괄처리를 한다고 알고 있었는데 그것만으로는 현재 발생한 문제(개요)에 대해서 명쾌한 해답을 주지 못해 좀 더 찾아보았습니다

useState의 여러 특징들과 처리방식을 참고하여 정리를 해보았고 흐름도를 통하여 결론지었습니다.

결론은 useState의 성능을 끌어올리기 위한 특징과 불변성이라는 개념때문에 발생한 문제로 정의를 하였고 이해되지 않는 부분을 스스로 설명하고 정리해보며 매듭지었습니다

 

정확하게는 함수형 업데이트를 이해하였습니다

 

Why React useState with functional update form is needed?

I'm reading React Hook documentation about functional updates and see this quote: The ”+” and ”-” buttons use the functional form, because the updated value is based on the previous value Bu...

stackoverflow.com

개요

블로그 팀 프로젝트를 진행하던 중 같이 진행하는 한 팀원이 왜 문제인지를 모르겠다며 찾아온 문제가 있었습니다. 문제가 무엇이었냐면 이미지(썸네일)를 넣을 수 있는 하나의 input에서 이미지를 넣었을 때 base64로 인코딩한 값과 이미지 이름이 들어가야되는데 base64값이 들어가면 파일이름이 없고, 파일이름이 들어가면 base64값이 없어진다라는게 문제였습니다.

아래부터 다음의 코드입니다

단순한 문제

<input
	ref={input}
	type="file"
	name="imgFile"
	id="imgFile"
	onChange={handleChangeFile}
/>
const [postDetailData, setPostDetailData] = useState({
    title: '',
    content: '',
    hashtag: '',
    base64Thumbnail: '',
    fileName: '',
  });

const handleChangeFile = (e: any) => {
    const reader = new FileReader();
    reader.onloadend = () => {
      const base64 = reader.result?.toString();
      if (base64) {
        setPostDetailData(() => {
          return { ...postDetailData, base64Thumbnail: base64.split(',')[1] };
        });
      }
    };
    if (input?.current?.files) {
      const selectFile = input.current.files[0];
      if (selectFile) {
        setFileImage(URL.createObjectURL(selectFile));
        setPostDetailData(() => {
          return {
            ...postDetailData,
            fileName: selectFile.name,
          };
        });
        reader.readAsDataURL(selectFile);
        setFile(e.target.files[0]);
      }
    }
  };

코드를 보며 설명을 듣자마자 어느 부분이 문제였는지 바로 알았습니다. “너가 setState로 변경하는 부분에서 Object를 변경하기 위해 콜백함수로 넘겨줄때, 콜백함수의 첫 번째 인자값이 현재 state에 이전값을 가져오는 부분이고 너가 그 이전값으로 넘겨줘야 정상동작을 할 수 있다”라고 말해줬고 저렇게 state 자체를 변경값 넘겨주면 어떤 에러가 발생할지 모른다는 말을 해주었습니다 .

const [postDetailData, setPostDetailData] = useState({
    title: '',
    content: '',
    hashtag: '',
    base64Thumbnail: '',
    fileName: '',
 });

// 위에서 아래로 실행
// 에러 수정 전 
setPostDetailData(() => {
	 return { ...postDetailData, base64Thumbnail: base64.split(',')[1] };
 });

const selectFile = input.current.files[0];

setPostDetailData(() => {
	return {
		...postDetailData,
		fileName: selectFile.name,
	};
});

// 에러 수정 후
setPostDetailData((prev) => {
	 return { ...prev, base64Thumbnail: base64.split(',')[1] };
 });

const selectFile = input.current.files[0];

setPostDetailData((prev) => {
	return {
		...prev,
		fileName: selectFile.name,
};

진짜 문제(궁금증)

해결은 했지만 그 친구가 질문하길 콜백함수가 받는 **prev랑 이미 이전값을 가지고 있는 postDetailData 가 어떤부분에서 다른지 질문하는 것**이었고 막상 생각해봤을 때 둘의 차이가 무엇인지에 대해서는 생각해보지 않았던 부분이라서 이전에 알고있던 지식들을 토대로 단순히 일괄처리로 진행 중일 때의 상태와 그냥 코드가 실행될 때의 상태에 따라 달라지는 부분이 아닐까? 하는 두루뭉실한 대답만 주었고 그 친구는 대충 알겠다며 넘어갔지만 저는 조금 걸리는 부분들이 있어서 문서화 시켜놓기로 했습니다.

문제 해결 과정

우선 useState의 특징과 처리방식을 하나씩 살펴보고자 했습니다.

 

특징 1) 일괄처리(batch)

  • 리렌더링을 최소화시켜 성능적인 문제가 발생하지 않도록 일괄처리 방식으로 진행
    • 이 방식은 브라우저에서 비동기적으로 처리하는 방식처럼 브라우저의 스택이 모두 비워졌을 때 작업을 시작

특징 2) 불변성

let a = [1,2,3,4,5]
let b = a

a.push(6) // push는 배열 맨 뒤에 값을 넣어주는 api

//console.log(a) ==> [1,2,3,4,5,6]
//(1) 그렇다면 b의 값은?

let a = [1,2,3,4,5]
let b = [...a]

a.push(6)
//(2)그렇다면 b의 값은?

(1) [1,2,3,4,5,6] (2) [1,2,3,4,5] 값에 의한 호출, 참조에 의한 호출자료형에 따라 다름!

주로 Object , Array 자료형은 참조에 의한 호출(저장되어 있는 메모리 주소를 넘긴다)

  • 불변성 (함수형 프로그래밍에서 중요하게 여겨지는 개념!! - useState만의 특징은 아님!)
    • 메모리 영역에서의 직접적인 변경이 불가능하다 ⇒ 값을 직접적인 변경이 불가능함(const)
    • 불변성을 지키며 값을 변경하는 방법이 ... spread 연산자로 불리며 참조 타입(array, objcect 등등)의 데이터를 복사해서 넣어주게 됨 ⇒ 참조 타입은 기본적으로 주소가 저장되고 주소를 참조하여 접근된다는 특징때문에 사용
      • 이러한 특성때문에 object 값을 단순히 변경시키는게 아닌 새로운 object로 복제 시킨뒤 그 값을 변경시켜서 전달해야 state가 감지하여 리렌더링
      • 복사본을 사용하지 않고 참조 타입을 그대로 옮긴다면 변화를 감지하지 못한다 (주의해야하는 부분) ⇒ 불변성을 지키지 않으면 에러로 규정

이렇게까지 불변성을 지켜야하는 이유는 무엇일까?

 

외부의 값을 함부로 변경할 수 있는 것은 위험한 일입니다. 만약 다른 어떤 곳에서 원본데이터를 사용하고 있다고 하면 어플리케이션 어딘가에서 사이드 이펙트가 일어날 가능성이 있기 때문입니다. 결국 리액트는 불변성을 지킴으로 인해 효과적인 상태 업데이트와 사이드 이펙트를 방지하는 이점들을 얻고 있습니다.

 

따라서 이 특징만을 잘 종합해봤더니 정리 된 자료가 있었습니다. 꽤 예전 자료이지만 동일한 문제에 대해서 설명하고 있기 때문에 참고하여 작성하였습니다

 https://hsp0418.tistory.com/171

When to use functional setState

 

정리하자면..

const [postDetailData, setPostDetailData] = useState({
    title: '',
    content: '',
    hashtag: '',
    base64Thumbnail: '',
    fileName: '',
 });

setPostDetailData(() => {
	 return { ...postDetailData, base64Thumbnail: base64.split(',')[1] };
 });

const selectFile = input.current.files[0];

setPostDetailData(() => {
	return {
		...postDetailData,
		fileName: selectFile.name,
	};
});

setState를 통해 state 변경을 시도하고자 할 때 useState는 리렌더링을 최소화시켜 성능을 향상시키기위해 일괄처리(batch)방식으로 처리 후 변경값이 있다면 리렌더링 시킵니다. 이때 에러가 발생하는 코드에서 접근하는 방식은 리액트 훅이 아직 처리하지 않은 state값을 가지고 있을 뿐만 아니라 실제로 변경됐다 한들 그 값은 기존의 state가 갖고있는 주소가 아닙니다. 그렇기 때문에 이 값을 동일한 사이클에서 뒤에 처리하는 호출이 앞부분 호출이 처리한 값을 무시해버리기 때문에 발생하는 문제라고 정의할 수 있습니다.

 

발생된 문제의 흐름도

  1. 현재 state를 업데이트하는 setState(set함수)는 일괄적으로 처리된 뒤 리렌더링 됨
  2. 따라서 모든 set함수가 완료되고 리렌더링 전까지는 postDetailData의 상태는 업데이트 되지 않는다
  3. 그런데 사이클에서 두번의 setState가 업데이트 되지않는 현재 postDetailData의 값을 가져다 써버리게 되니 두번째로 호출되는 set함수가 앞선 set함수에서 실행한 데이터를 덮어버린다.
  4. 이 과정이 하나의 변수에서 이루어지는 것이 아닌 계속 새로운 변수를 복사해서 생성시키며 불변성을 지키는 것이기 때문에 조금 헷갈릴 수 있다
    1. (참고) 만약에 직접적으로 값을 변경할 수 있었다면 발생할 문제는 아니긴하다
    2. prev는 되는데 PostDetailData state는 왜안되는데?의 정답
  5. 따라서 prev는 단순한 이전 상태값이 아닌 앞선 set함수가 작업한 데이터의 주소라고 이해하면 될거같다
    1. (참고) setState를 함수로 처리하게 되면 동기적으로 처리된다고 한다

evan 오브젝트를 변경시키고자 한다면 evan을 복사시킨뒤 -> 새로 만든 다음 name을 수정 후 전달

https://evan-moon.github.io/2020/01/05/what-is-immutable/

 

변하지 않는 상태를 유지하는 방법, 불변성(Immutable)

이번 포스팅에서는 순수 함수에 이어 함수형 프로그래밍에서 중요하게 여기는 개념인 에 대한 이야기를 해보려고 한다. 사실 순수 함수를 설명하다보면 불변성에 대한 이야기가 꼭 한번은 나오

evan-moon.github.io

회고

불변성이라는 생소한 개념때문에 놓친 부분들이 많았다고 생각합니다. 데이터를 변경시키는 작업이 이미 저장되어 있는 하나의 변수에서 계속 이뤄지는 것이라고 생각을 하게 된다면 차이를 이해하기 힘들지만 계속 복제시켜 새롭게 만들어낸다는 개념을 이해하게 된다면 어렵게 느껴지지 않을 거 같다고 생각합니다. 저는 그 개념을 받아들이는게 이해가 안되고 복잡해서 많은 자료를 참고하게 되었습니다.

마지막으로 스스로를 위한 공부가 아니라 잘 없는 기회로써 남에게 설명하고자 새로운 자료들을 공부하고 문서를 작성했던 과정이 힘들었지만 재밌었고 이후에도 더 쉽게 풀어보고자 계속 업데이트할 예정입니다

참고한 사이트

useState 비동기성

https://velog.io/@alstnsrl98/useState는-동기-비동기-동기적-처리

https://velog.io/@seongkyun/React의-setState가-비동기-처리되는-이유

 

useState 동작과정

https://velog.io/@jjunyjjuny/React-useState는-어떻게-동작할까

 

불변성에 관해

https://webigotr.tistory.com/293https://stackoverflow.com/questions/58860021/why-react-hook-usestate-uses-const-and-not-let

https://evan-moon.github.io/2020/01/05/what-is-immutable/

https://velog.io/@rimo09/useState-사용시-const를-사용할-수-있는-이유

https://hsp0418.tistory.com/171

 

웹브라우저 동작원리

https://haenny.tistory.com/243

 

state 사용법

https://wildcard.tistory.com/m/24

 

hook에 대해서

https://dev.to/kuldeeptarapara/react-hooks-best-practices-in-2022-4bh0

복사했습니다!