LabHub
배우기 러닝패스 코스

고래를 찾는데 고양이가 나타났다 · 과거의 응답이 오늘을 바꾸지 못하게 · 이론

취소 버튼은 시간 여행을 막지 못한다

LabHub 에서 이어서 보기

한 줄 요약

더 필요 없는 작업을 취소하는 일과, 늦게 도착한 결과가 화면을 바꾸지 못하게 하는 일은 별개의 책임이다. effect의 설정과 정리를 한 쌍으로 설계한다.

왜 이게 필요했나

고래 검색을 보내기 전에 고양이 요청에 abort를 호출했다. 이제 안전할까? 네트워크 요청은 멈췄어도 이미 끝난 응답의 후처리가 남아 있을 수 있고, 사용하는 비동기 도구가 취소 신호를 지원하지 않을 수도 있다. 상대가 취소를 지켜 줄 것이라는 기대만으로 화면의 정확성을 맡기면 문제가 다른 자리로 옮겨 간다.

이 실험의 전송 모형은 취소 신호를 받았다는 기록을 남긴다. 체크박스를 켜면 그 뒤에도 응답을 배달할 수 있다. 실제 HTTP 구현이라고 속이는 것이 아니라, 취소와 결과 무시를 구분할 수 있도록 일부러 통제한 Promise 모형이다. 네트워크 환경이 우연히 느려지기를 기다리지 않아도 같은 순서를 반복할 수 있다.

어떻게 동작하나

요청 A를 위한 effect가 설정되면 A만의 유효 기간이 시작된다. 의존성이 바뀌어 요청 B의 effect가 설정되기 전에는 A의 정리가 실행된다. 컴포넌트가 사라질 때도 정리가 필요하다. 정리에서는 두 질문에 답한다.

1. 이 effect가 만든 결과를 앞으로 화면에 반영해도 되는가?
2. 이 effect가 시작한 외부 작업을 더 실행할 필요가 있는가?

첫 질문은 active 플래그나 요청 식별자로 처리할 수 있다. 두 번째는 AbortController와 취소 신호를 지원하는 도구로 처리한다. 어떤 구현을 택하든 요청끼리 상태를 잘못 공유하지 않아야 한다. 오래된 요청의 정리가 최신 요청을 취소하면 사용자는 새 검색을 할수록 응답을 잃는다.

성공 경로만 막아서는 부족하다. A가 늦게 실패하고 B가 이미 성공했다면 A의 catch가 B의 화면에 오류를 띄울 수 있다. 반대로 B의 최신 실패 뒤에 A의 성공이 와도 최신 오류 설명이 사라지면 안 된다. 성공과 실패 모두 같은 유효성 정책을 따라야 한다.

| 정리 방식 | 과거 응답 덮어쓰기 | 불필요한 작업 취소 |
| --- | --- | --- |
| 결과만 무시 | 막을 수 있음 | 별도 처리가 필요 |
| 취소만 요청 | 도구의 취소 보장에 의존 | 지원하는 도구에서 가능 |
| 유효성 검사와 취소 | 두 책임을 명시적으로 처리 | 두 책임을 명시적으로 처리 |

빈 검색어도 생각해 보자. 사용자가 입력을 지우고 제출했다면 새 화면은 idle이다. 그전에 보낸 응답이 도착해 결과를 복원하면 사용자의 마지막 의도를 거스른다. 같은 단어를 다시 제출했을 때에도 제출 번호가 바뀌면 재시도할 수 있어야 한다.

active를 어디에 두는가

모든 요청이 공유하는 변수 하나를 active라고 부르면 이름은 맞아도 수명이 틀릴 수 있다. A를 정리하며 false로 바꾼 뒤 B를 시작하며 같은 변수를 true로 바꾸면, 늦게 도착한 A도 true를 읽는다. A의 종료 표시를 B의 시작이 지워 버린 셈이다. 각 effect 설정이 자기 변수를 캡처하게 하거나, 응답이 들고 온 요청 번호와 현재 요청 번호를 비교하는 방식이 필요하다.

이 차이를 손으로 추적해 보자. A 설정에서 만들어진 플래그를 A의 then과 catch가 읽는다. A 정리는 그 플래그만 false로 바꾼다. B 설정은 별도의 플래그를 만들고 B의 콜백은 그것을 읽는다. 두 콜백이 같은 이름의 변수를 사용해도 같은 저장 공간인 것은 아니다. 코드의 변수 이름보다 선언 위치와 클로저가 포획한 대상을 보아야 한다.

요청 번호 방식에서도 함정이 있다. 현재 번호를 증가시키지 않고 같은 검색어 문자열만 비교하면 동일 단어의 두 번 제출을 구별할 수 없다. 첫 번째 요청의 오래된 결과가 두 번째 재시도의 결과인 것처럼 들어올 수 있다. 검사에는 서로 다른 단어의 역순 응답뿐 아니라 같은 단어를 연속 제출한 경우도 넣는다.

정리의 효과를 확인할 때는 응답 완료를 무작정 기다리지 않는다. 취소 신호는 요청이 끝나지 않아도 확인할 수 있다. 과거 응답을 일부러 나중에 완료시켜 화면 보호를 보고, 별도로 aborted를 읽어 자원 정리를 확인한다. 관찰 대상을 나누면 어떤 책임이 빠졌는지도 더 정확하게 설명할 수 있다.

현장에서 만나는 모습

개발 StrictMode에서 effect의 설정·정리·재설정을 관찰할 수 있다. 첫 설정의 작업이 정리되지 않는다면 두 번째 설정과 함께 남는다. 이를 피하려고 정리 검사를 없애기보다, 설정과 정리를 반복해도 필요한 작업만 살아 있는지 확인한다. 운영 빌드에서 개발 점검과 같은 실행 횟수를 가정해서는 안 된다.

메모리 누수 경고가 없다고 정리가 된 것은 아니다. 화면이 사라진 뒤 상태 업데이트가 보이지 않더라도 요청과 이벤트 구독은 계속 살아 있을 수 있다. 검사에서는 DOM 결과뿐 아니라 전달한 signal의 aborted 상태도 관찰한다. 사용자가 창을 닫는 사건이 자원 사용의 끝과 연결되어야 한다.

직접 확인할 것

취소를 무시하는 모드로 성공·실패 응답을 각각 역순 배달한다. 그다음 취소를 지키는 모드에서 검색 화면을 해제한다. 결과가 맞는 것과 취소 신호가 전달된 것을 각각 확인하고, 둘 중 하나만 구현한 오답이 어느 검사에서 실패하는지 찾아보자.

원리는 [React의 effect 정리 설명](https://react.dev/learn/synchronizing-with-effects)을 참고한다. 이 실험의 수동 전송 모형과 사건 번호는 학습용 설계다.