[React] useState 렌더링과 setTimeout 콜백의 동시성

이상엽·2024년 7월 17일
1
post-thumbnail

배경

미션 설명

미션 링크

우아한테크코스의 미션 중 신용카드 정보를 입력하는 커스텀 훅을 만드는 미션을 진행했다.
위의 사진과 같이 카드사 식별 번호에 따른 포매팅을 적용하였다.

카드 브랜드 구분 로직 (Diners / AMEX / UnionPay / etc)

Diners: 36으로 시작하는 14자리 숫자
- 예시: 3612 345678 9012
    
AMEX: 34, 37로 시작하는 15자리 숫자
- 예시 (34로 시작): 3412 345678 90123
- 예시 (37로 시작): 3712 345678 90123

유니온페이: 카드의 앞 번호가 아래 3가지 조건을 만족하는 16자리 숫자
- 622126~622925로 시작하는 경우: 6221 2612 3456 7890
- 624~626로 시작하는 경우: 6240 1234 5678 9012
- 6282~6288로 시작하는 경우: 6282 1234 5678 9012

etc: 그 외 나머지
- 예시: 1234 5678 2345 5678

접근방법

해당 코드 보러 가기

  1. 커스텀 훅의 React.ChangeEvent 함수에서 유저가 입력한 카드번호(e.target.value) 값을 가져온다.
  2. 카드 번호에 있는 공백을 제거한다.
  3. 유효성 검사를 진행한다.
  4. 카드 브랜드를 확인하여 브랜드에 적절한 포매팅(공백)을 적용한다.

그래서 클라이언트가 123456789 을 입력하면 1234 5678 9 로 자동 포멧팅이 된 값을 확인할 수 있게 된다.

문제상황


잘 보면 포매팅이 적용되는 순간부터 마우스 커서가 가장 뒤로 이동하는 것을 확인할 수 있다.

포매팅이 적용되면서 원래 state 값이 모두 변경되었기에 기존 커서 위치를 유지하지 못하고 가장 뒤로가게 되는 문제로 사용자 경험을 저하시키는 문제가 있었다.

문제 해결방법

그렇다면 커서 위치를 수동으로 계산하여 이동시켜줘야한다. 그래서 카드 번호 입력하는 커스텀 훅 내에 커서 위치를 계산하여 커서를 위치시키는 로직을 추가하였다.

const handleCardNumberChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { value, selectionStart } = e.target;
	... 
    
    setTimeout(() => {	// 커서 위치를 계산하여 위치시키는 로직
      if (selectionStart === null) return;

      if (formattedValue[selectionStart - 1] === " ") {
        const newSelectionStart =
          formattedValue.length > value.length ? selectionStart + 1 : selectionStart - 1;

        return e.target.setSelectionRange(newSelectionStart, newSelectionStart);
      }
      e.target.setSelectionRange(selectionStart, selectionStart);
    });
}

// selectionStart : 현재 유저의 커서 위치값
// value : 사용자의 입력값
// formattedValue : 입력값을 카드 브랜드에 맞게 새로 포매팅 적용된 값

그래서 handleCardNumberChange 훅에서 의도한 로직은

1. 유저가 값을 입력하면 커스텀 훅이 동작한다.
2. 유효성 검사 및 새로 포매팅을 적용한다.
3. 새로운 포매팅된 값으로 state 를 변경한다.
4. setTimeout 으로 커서 위치를 계산하여 커서 위치를 수동으로 위치시킨다.

내가 의도한 바는 useState 로 인해 input 값이 새로 리렌더링 후 setTimeout 의 콜백함수가 처리가 되어 커서 위치가 변경되는건데, 그럼 이때 반드시 setState 의 리렌더링 후 setTimeout 의 콜백함수가 동작하게 될까?

useState 렌더링과 setTimeout 콜백의 동시성

앞에 서론이 길었는데 직접 실험을 통해 해당 결과를 도출해보았다.

언뜻보면 문제가 없어보이는데 잘 보면 1234 5 가 있을 때 공백 앞 4에서 값을 입력했을 때 커서 위치가 어떤 경우에는 공백 앞에 위치하고 어떤 경우에는 공백 뒤에 위치하는 것을 확인할 수 있었다.

이러한 결과가 나오는 이유는 어떤 경우에는 setState 로 인한 렌더링이, 어떤 경우에는 setTimeout 의 콜백이 처리되기에 어떤게 먼저 처리되는지에 대한 보장이 없기 때문이다.

결론

우선 setState 로 값이 변경이 된 후 race condition 이 작동한다.

  1. state 값이 변경되어 리렌더링 과정 (reflow, repaint 처리)
  2. setTimeout 의 콜백 함수가 task queue 에서 stack 으로 이동

해당 두 동작중에 먼저 처리되는 순서대로 동작이 진행된다.
(무엇이 먼저 실행될지에 대한 순서 보장 ❌)

그러니 setState와 setTimeout 을 같이 이용하여 처리하는게 아닌 안전하게 useEffect를 이용하여 구현하는게 좋다!


해당 내용에 대한 PR Comment 주소

0개의 댓글

관련 채용 정보