양식 제출을 향상하기 위해 JavaScript fetch
API를 사용한 적이 있다면 실수로 중복 요청/경합 조건 버그가 발생했을 가능성이 높습니다. 오늘은 문제와 이를 방지하기 위한 권장 사항을 안내해 드리겠습니다.
(원하는 경우 마지막에 비디오)
아주 기본적인 것을 생각해 봅시다.
<form method="post"> <label for="name">Name</label> <input id="name" name="name" /> <button>Submit</button> </form>
제출 버튼을 누르면 브라우저는 전체 페이지를 새로 고칩니다.
제출 버튼을 클릭한 후 브라우저가 어떻게 다시 로드되는지 확인하세요.
페이지 새로 고침이 항상 사용자에게 제공하려는 환경은 아니므로 일반적인 대안은 다음을 사용하는 것입니다.fetch
API를 사용하여 양식 데이터를 제출합니다.
단순한 접근 방식은 아래 예와 같을 수 있습니다.
페이지(또는 구성 요소)가 마운트된 후 양식 DOM 노드를 가져오고 양식을 사용하여 fetch
요청을 구성하는 이벤트 리스너를 추가합니다.preventDefault()
메소드를 호출합니다.
const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); function handleSubmit(event) { const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }); event.preventDefault(); }
이제 JavaScript 핫샷이 GET과 POST 및 요청 본문에 대해 나에게 트윗을 시작하기 전에fetch
요청이 주요 초점이 아니기 때문에 의도적으로 단순하게 유지하고 있습니다.
여기서 중요한 문제는 event.preventDefault()
입니다. 이 방법을 사용하면 브라우저가 새 페이지를 로드하고 양식을 제출하는 기본 동작을 수행하지 못합니다.
이제 화면을 보고 제출을 누르면 페이지가 다시 로드되지 않지만 네트워크 탭에 HTTP 요청이 표시되는 것을 볼 수 있습니다.
브라우저는 전체 페이지를 다시 로드하지 않습니다.
불행히도 기본 동작을 방지하기 위해 JavaScript를 사용함으로써 실제로 기본 브라우저 동작에 없는 버그가 도입되었습니다.
일반을 사용하는 경우
이를 JavaScript 예제와 비교하면 모든 요청이 전송되고 취소되지 않고 모두 완료되는 것을 볼 수 있습니다.
각 요청에 소요되는 시간은 다르지만 시작된 순서와 다른 순서로 해결될 수 있으므로 이는 문제가 될 수 있습니다. 이는 해당 요청 해결에 기능을 추가하면 예상치 못한 동작이 발생할 수 있음을 의미합니다.
예를 들어, 각 요청에 대해 증가하는 변수(“ totalRequestCount
“)를 생성할 수 있습니다. handleSubmit
함수를 실행할 때마다 총 개수를 늘릴 수 있을 뿐만 아니라 현재 요청을 추적하기 위해 현재 숫자를 캡처할 수도 있습니다(“ thisRequestNumber
”).
fetch
요청이 해결되면 해당 번호를 콘솔에 기록할 수 있습니다.
const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); let totalRequestCount = 0 function handleSubmit(event) { totalRequestCount += 1 const thisRequestNumber = totalRequestCount const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }).then(() => { console.log(thisRequestNumber) }) event.preventDefault(); }
이제 제출 버튼을 여러 번 부수면 콘솔에 2, 3, 1, 4, 5 순서로 다른 숫자가 인쇄되는 것을 볼 수 있습니다. 이는 네트워크 속도에 따라 다르지만 모두 동의할 수 있다고 생각합니다. 이것이 이상적이지 않다는 것입니다.
사용자가 연속해서 여러 fetch
요청을 트리거하고 완료되면 애플리케이션이 변경 사항으로 페이지를 업데이트하는 시나리오를 생각해 보세요. 요청이 순서대로 해결되지 않아 결국 사용자는 부정확한 정보를 보게 될 수 있습니다.
브라우저가 이전 요청을 취소하고 가장 최근 요청이 완료된 후 페이지를 로드하여 최신 버전을 로드하기 때문에 이는 JavaScript가 아닌 세계에서는 문제가 되지 않습니다. 그러나 페이지 새로 고침은 그다지 매력적이지 않습니다.
JavaScript 애호가에게 좋은 소식은 두 가지 모두를 가질 수 있다는 것입니다.
우리는 좀 더 발품을 팔면 됩니다.
fetch
API 문서를 보면 AbortController
및 fetch
옵션의 signal
속성을 사용하여 가져오기를 중단할 수 있음을 알 수 있습니다. 다음과 같이 보입니다.
const controller = new AbortController(); fetch(url, { signal: controller.signal });
AbortContoller
의 신호를 fetch
요청에 제공함으로써 AbortContoller
의 abort
메소드가 트리거될 때마다 요청을 취소할 수 있습니다.
JavaScript 콘솔에서 더 명확한 예를 볼 수 있습니다. AbortController
를 생성하고 fetch
요청을 시작한 다음 즉시 abort
메서드를 실행해 보세요.
const controller = new AbortController(); fetch('', { signal: controller.signal }); controller.abort()
콘솔에 예외가 인쇄되는 것을 즉시 볼 수 있습니다. Chromium 브라우저에서는 "Uncaught (in promise) DOMException: The user aborted a request."라는 메시지가 표시되어야 합니다. 그리고 네트워크 탭을 탐색하면 상태 텍스트 "(취소됨)"과 함께 실패한 요청이 표시됩니다.
이를 염두에 두고 양식의 제출 핸들러에 AbortController
를 추가할 수 있습니다. 논리는 다음과 같습니다.
AbortController
확인하세요. 존재하는 경우 중단하십시오.
AbortController
를 만듭니다.
AbortController
제거합니다.
이를 수행하는 방법에는 여러 가지가 있지만 제출된 각 <form>
DOM 노드와 해당 AbortController
간의 관계를 저장하기 위해 WeakMap
을 사용하겠습니다. 양식이 제출되면 그에 따라 WeakMap
확인하고 업데이트할 수 있습니다.
const pendingForms = new WeakMap(); function handleSubmit(event) { const form = event.currentTarget; const previousController = pendingForms.get(form); if (previousController) { previousController.abort(); } const controller = new AbortController(); pendingForms.set(form, controller); fetch(form.action, { method: form.method, body: new FormData(form), signal: controller.signal, }).then(() => { pendingForms.delete(form); }); event.preventDefault(); } const forms = document.querySelectorAll('form'); for (const form of forms) { form.addEventListener('submit', handleSubmit); }
중요한 것은 중단 컨트롤러를 해당 양식과 연결할 수 있다는 것입니다. 양식의 DOM 노드를 WeakMap
의 키로 사용하는 것이 이를 수행하는 편리한 방법입니다.
이를 사용하여 AbortController
신호를 fetch
요청에 추가하고, 이전 컨트롤러를 중단하고, 새 컨트롤러를 추가하고, 완료 시 삭제할 수 있습니다.
바라건대, 그 모든 것이 의미가 있습니다.
이제 해당 양식의 제출 버튼을 여러 번 부수면 가장 최근 요청을 제외한 모든 API 요청이 취소되는 것을 볼 수 있습니다.
이는 해당 HTTP 응답에 응답하는 모든 함수가 예상한 대로 더 많이 작동한다는 것을 의미합니다.
이제 위와 동일한 계산 및 로깅 논리를 사용하면 제출 버튼을 7번 부술 수 있으며 콘솔에서 6개의 예외( AbortController
로 인해)와 "7" 로그 1개를 볼 수 있습니다.
다시 제출하고 요청이 해결될 때까지 충분한 시간을 허용하면 콘솔에 "8"이 표시됩니다. 제출 버튼을 여러 번 누르면 예외와 최종 요청 수가 올바른 순서로 계속 표시됩니다.
요청이 중단될 때 콘솔에 DOMException이 표시되지 않도록 로직을 더 추가하려면 fetch
요청 뒤에 .catch()
블록을 추가하고 오류 이름이 " AbortError
"와 일치하는지 확인할 수 있습니다.
fetch(url, { signal: controller.signal, }).catch((error) => { // If the request was aborted, do nothing if (error.name === 'AbortError') return; // Otherwise, handle the error here or throw it back to the console throw error });
이 전체 게시물은 JavaScript로 강화된 양식에 중점을 두었지만 fetch
요청을 생성할 때마다 AbortController
를 포함하는 것이 좋습니다. 아직 API에 내장되어 있지 않다는 점은 정말 안타깝습니다. 하지만 이것이 이를 포함하는 좋은 방법을 보여주기를 바랍니다.
이 접근 방식은 사용자가 제출 버튼을 여러 번 스팸하는 것을 방지하지 못한다는 점도 언급할 가치가 있습니다. 버튼은 여전히 클릭 가능하고 요청은 계속 실행됩니다. 이는 응답을 처리하는 보다 일관된 방법을 제공할 뿐입니다.
불행하게도 사용자가 제출 버튼을 스팸으로 보내는 경우 해당 요청은 여전히 백엔드로 이동하여 불필요한 리소스를 많이 소비할 수 있습니다.
일부 순진한 솔루션은 다음을 사용하여 제출 버튼을 비활성화할 수 있습니다.
스크립트 요청을 통해 남용을 해결하지 않습니다.
서버에 대한 너무 많은 요청으로 인한 남용을 해결하려면 다음 중 일부를 설정하는 것이 좋습니다.
또한 속도 제한은 중복 요청, 경쟁 조건 및 일관되지 않은 UI 업데이트의 원래 문제를 해결하지 못한다는 점도 언급할 가치가 있습니다. 이상적으로는 양쪽 끝을 모두 사용하여 양쪽 끝을 모두 덮어야 합니다.
어쨌든, 오늘은 그게 전부입니다. 동일한 주제를 다루는 동영상을 보고 싶다면 이 동영상을 시청하세요.
읽어주셔서 정말 감사합니다. 이 글이 마음에 드셨다면 추천 부탁드립니다
원래 게시 날짜