overreacted 블로그 포스트 번역
성능에 민감한 코드를 작성할 때는 알고리즘의 복잡도를 염두에 두는 것이 좋습니다. 이는 보통 Big-O 표기법으로 표현됩니다. Big-O는 더 많은 데이터를 입력할 때 코드가 얼마나 느려지는지를 측정합니다. 예를 들어, 정렬 알고리즘이 O(n²) 복잡도를 가지고 있다면, 50배 많은 항목을 정렬할 때는 대략 50² = 2,500배 느려집니다. Big-O는 정확한 숫자를 제공하지는 않지만, 알고리즘이 어떻게 확장되는지 이해하는 데 도움이 됩니다.
예를 들면: O(n), O(n log n), O(n²), O(n!).
그러나 이 글은 알고리즘이나 성능에 관한 것이 아닙니다. API와 디버깅에 관한 것입니다. API 설계도 매우 유사한 고려사항을 포함한다는 것이 밝혀졌습니다.
저희는 코드의 실수를 찾고 수정하는 데 상당한 시간을 소비합니다. 대부분의 개발자는 버그를 더 빨리 찾고 싶어 합니다. 결국 만족스러울 수 있지만, 로드맵에 구현할 수 있는 다른 일이 많을 때 하루 종일 하나의 버그를 추적하는 데 시간을 보내는 것은 답답합니다.
디버깅 경험은 저희가 선택하는 추상화, 라이브러리, 도구에 영향을 미칩니다. 일부 API와 언어 설계는 특정 종류의 실수를 불가능하게 만듭니다. 일부는 끝없는 문제를 만듭니다. 하지만 어느 것이 좋은지 어떻게 알 수 있을까요?
저는 이것을 생각하는 데 도움이 되는 지표를 가지고 있습니다. 저는 이것을 Bug-O 표기법이라고 부릅니다:
🐞(n)
Big-O는 입력이 증가함에 따라 알고리즘이 얼마나 느려지는지를 설명합니다. Bug-O는 코드베이스가 증가함에 따라 API가 당신을 얼마나 느리게 만드는지를 설명합니다.
예를 들어, node.appendChild()와 node.removeChild() 같은 명령형 작업으로 시간에 따라 DOM을 수동으로 업데이트하고 명확한 구조가 없는 다음 코드를 생각해 봅시다:
이 코드의 문제는 "못생겼다"는 것이 아닙니다. 저희는 미학에 대해 이야기하고 있지 않습니다. 문제는 이 코드에 버그가 있다면, 어디서부터 찾기 시작해야 할지 모른다는 것입니다.
콜백과 이벤트가 발생하는 순서에 따라, 이 프로그램이 취할 수 있는 코드 경로의 수가 조합론적으로 폭발합니다. 일부에서는 올바른 메시지를 볼 것입니다. 다른 것들에서는 여러 개의 스피너, 실패 및 오류 메시지가 함께 표시되고 충돌이 발생할 수도 있습니다.
이 함수는 4개의 서로 다른 섹션을 가지고 있으며 순서에 대한 보장이 없습니다. 저의 매우 비과학적인 계산에 따르면 4×3×2×1 = 24개의 서로 다른 순서로 실행될 수 있습니다. 4개의 코드 세그먼트를 더 추가하면 8×7×6×5×4×3×2×1 — 4만 개의 조합이 됩니다. 그것을 디버깅하는 데 행운을 빕니다.
다시 말해, 이 접근 방식의 Bug-O는 🐞(n!)입니다. 여기서 n은 DOM을 건드리는 코드 세그먼트의 수입니다. 네, 그것은 팩토리얼입니다. 물론, 저는 여기서 매우 과학적이지 않습니다. 실제로는 모든 전환이 가능하지는 않습니다. 하지만 반면에 이러한 각 세그먼트는 두 번 이상 실행될 수 있습니다. 🐞(¯_(ツ)_/¯)가 더 정확할 수 있지만 여전히 매우 나쁩니다. 분명히 더 나은 방법이 있습니다.
이 코드의 Bug-O를 개선하려면 가능한 상태와 결과의 수를 제한할 수 있습니다. 이를 위해 라이브러리가 필요하지는 않습니다. 코드에 구조를 강제하는 문제일 뿐입니다. 다음은 저희가 할 수 있는 한 가지 방법입니다:
이 코드는 너무 다르게 보이지 않을 수 있습니다. 심지어 조금 더 장황합니다. 하지만 이 줄 때문에 디버깅하기가 극적으로 더 간단합니다:
폼 상태를 지운 후 조작을 수행하기 전에, 저희는 DOM 작업이 항상 처음부터 시작되도록 보장합니다. 이것이 저희가 불가피한 엔트로피와 싸울 수 있는 방법입니다 — 실수가 누적되도록 허용하지 않음으로써. 이것은 "껐다가 다시 켜기"의 코딩 등가물이며, 놀랍도록 잘 작동합니다. 출력에 버그가 있다면, 저희는 한 단계 뒤로만 생각하면 됩니다 — 이전 setState 호출까지. 렌더링 결과를 디버깅하는 Bug-O는 🐞(n)입니다. 여기서 n은 렌더링 코드 경로의 수입니다. 여기서는 4개일 뿐입니다 (switch에 4개의 경우가 있기 때문입니다).
저희는 여전히 상태 설정에서 경쟁 조건을 가질 수 있지만, 각 중간 상태를 기록하고 검사할 수 있기 때문에 디버깅하기가 더 쉽습니다. 저희는 또한 명시적으로 원하지 않는 전환을 허용하지 않을 수 있습니다:
물론, 항상 DOM을 재설정하는 것은 트레이드오프가 있습니다. 매번 DOM을 순진하게 제거하고 다시 만드는 것은 내부 상태를 파괴하고, 포커스를 잃고, 더 큰 애플리케이션에서 끔찍한 성능 문제를 야기할 것입니다.
이것이 React 같은 라이브러리가 도움이 될 수 있는 이유입니다. 이들은 실제로 그렇게 하지 않으면서도 항상 UI를 처음부터 다시 만드는 패러다임으로 생각할 수 있게 해줍니다:
코드는 다르게 보일 수 있지만, 원칙은 동일합니다. 컴포넌트 추상화는 경계를 강제하므로 페이지의 다른 코드가 DOM이나 상태를 건드릴 수 없다는 것을 알 수 있습니다. 컴포넌트화는 Bug-O를 줄이는 데 도움이 됩니다.
실제로, React 앱의 DOM에서 어떤 값이 잘못되어 보인다면, React 트리에서 그 위의 컴포넌트 코드를 하나씩 살펴봄으로써 어디서 왔는지 추적할 수 있습니다. 앱 크기에 관계없이, 렌더링된 값을 추적하는 것은 🐞(트리 높이)입니다.
다음 번에 API 토론을 볼 때, 다음을 고려해 보세요: 그것에서 일반적인 디버깅 작업의 🐞(n)은 무엇입니까? 저희가 깊이 있게 알고 있는 기존 API와 원칙은 어떨까요? Redux, CSS, 상속 — 이들 모두 자신의 Bug-O를 가지고 있습니다.