(번역) 세분화된 반응형(Fine-Grained Reactivity)에 대한 핸즈온 소개
원문: https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf
반응형 프로그래밍은 수년간 존재해왔지만, 유행이었다 다시 사라지는 것으로 보입니다. 자바스크립트 프런트엔드에서는 지난 몇 년간 다시 상승세를 보였습니다. 프레임워크를 초월해 모든 개발자에게 친숙하고 유용한 주제입니다.
그러나 항상 모든 반응형이 쉬운 것은 아닙니다. 먼저, 다양한 유형의 반응형이 있습니다. 용어와 명명은 종종 다른 사람들에게 동일한 단어이지만 다른 의미로 오버로딩되곤 합니다.
두 번째로, 반응형은 때로는 마술처럼 보입니다. 실제로 그렇지는 않지만, “무엇”을 이해하기 전에 “어떻게”에 관심을 둬 혼란스러울 수 있습니다. 이는 실제 사례로 가르치는 것을 도전적으로 만들고, 너무 이론적으로 접근하는 것을 방지하기 위해 세심하게 균형을 지키는 역할도 합니다.
이 글은 “어떻게”에 초점을 맞추지 않을 것입니다. 대신 MobX, Vue, Svelte, Knockout 및 Solid와 같은 라이브러리에서 사용하는 세분화된 반응형에 대해 부드럽게 소개해보려 합니다.
참고: 이 글은 많은 분들이 친숙한 RxJS 같은 스트림 기반의 반응형과 다를 수 있습니다. 관련이 있고 비슷한 점도 있지만 완전히 같지는 않습니다.
이 글은 세분화된 반응형 또는 일반적으로 이야기하는 반응형에 대해 처음 접하는 사람을 대상으로 했지만, 자바스크립트에 대한 지식과 일부 컴퓨터 과학 개론에 익숙하다고 가정하고 시작하는 중급 수준의 글입니다. 최선을 다해 자세히 설명드리겠지만 질문이 있다면 댓글로 남겨주세요.
또한, 코드 샌드박스에 코드 스니펫과 예제를 올릴 예정입니다. 제 라이브러리 Solid를 사용해 예제를 구동할 것이고 이 글의 구문 또한 Solid를 사용할 것입니다. 그러나 대부분의 라이브러리 모두 비슷합니다. 완전한 대화형 환경에서 실행하려면 링크를 누르세요.
선수 입장
세분화된 반응형은 원시(primitive) 네트워크에서 구성됩니다. 여기서 말하는 원시는 숫자 같은 자바스크립트의 원시 타입을 이야기하는 것이 아니라 Promises
같은 간단한 구조체를 의미합니다.
각각은 그래프에서 노드 역할을 합니다. 이상적인 전기회로라고 생각하면 됩니다. 모든 변경사항은 동시에 모든 노드에 적용됩니다. 단일 시점에서의 동기화를 통해 모든 문제가 해결됩니다. 이 단일 시점이 사용자 인터페이스를 구축할 때 자주 사용하게 될 공간입니다.
먼저 다양한 원시 타입을 알아보는 것부터 시작해 보겠습니다.
시그널
시그널은 반응형 시스템에서 가장 기본적인 타입입니다. getter, setter 그리고 값으로 구성됩니다. 학술지에서는 종종 시그널이라 부르지만, Observable, Atom, Subject 또는 Ref라고도 합니다.
물론 이 자체만으로 흥미롭진 않습니다. 시그널은 무엇이든 저장할 수 있는 값입니다. 중요 세부 사항은 get과 set 모두 임의의 코드를 실행할 수 있다는 것입니다. 이 점은 업데이트를 전파하는 데 중요합니다.
함수는 이를 수행하는 주요 방법이지만 객체의 getter 또는 프록시를 통해 수행하는 것도 확인할 수 있습니다.
또는 컴파일러의 뒤에서 동작하곤 합니다.
시그널의 핵심은 이벤트 이미터(emitter)입니다. 그러나 주요 차이점은 구독을 관리하는 방법입니다.
리액션
시그널은 그의 파트너인 리액션 없이는 그다지 흥미롭지 않습니다. 리액션은 Effect, Autoruns, Watches 또는 Computed라고 불립니다. 리액션은 시그널을 관찰(observe)하고 값이 업데이트될 때마다 시그널을 다시 실행합니다.
아래 예시는 처음 그리고 시그널이 업데이트될 때마다 실행되는 래핑 된 함수 표현식 입니다.
이런 동작을 처음 보면 마술처럼 느껴지겠지만, 이 동작을 하기 위해 우리 시그널에 getter가 필요한 것입니다. 시그널이 실행될 때마다 래핑 함수가 시그널을 감지하고 자동으로 구독합니다. 이 동작은 이후에 더 자세히 설명하겠습니다.
중요한 것은 이런 시그널이 모든 종류의 데이터를 전달할 수 있고 리액션이 데이터로 무엇이든지 할 수 있다는 것입니다. 코드 샌드박스 예제에서는 페이지에 DOM 요소를 붙이는 사용자 지정 로그 함수를 작성했습니다. 저희는 이걸로 모든 업데이트를 조정할 수 있습니다.
두 번째로, 업데이트가 동기적으로 발생합니다. 다음 명령을 로그로 나타내기 전에 리액션이 이미 실행되었습니다.
네 이게 전부입니다. 세분화된 반응형에 대한 모든 도구를 소개했습니다. 시그널과 리액션. 관찰 대상과 관찰자입니다. 사실 이 두 가지만으로 대부분의 동작을 생성합니다. 그러나 우리가 이야기 해야할 또 다른 핵심 원시 타입이 있습니다.
파생(Derivations)
종종 저희는 데이터를 다른 방식으로 표현하고 여러 리액션에 같은 시그널을 사용해야 할 수 있습니다. 그러기 위해 리액션에 작성하거나 헬퍼를 별도로 작성할 수 있습니다.
참고: 이 예에서
fullName
은 함수입니다. Effect내에서 시그널을 읽으려면 Effect가 실행될 때까지 실행을 연기해야 하기 때문입니다. 단순한 값이라면 추적하거나 Effect를 다시 실행할 기회가 없습니다.
그러나 때로는 파생된 값의 계산 비용이 비쌀 수도 있고 다시 수행하고 싶지 않을 수 있습니다. 이런 이유로 중간 계산을 자체 시그널로 저장하기 위해 함수 메모이제이션과 유사한 역할을 하는 세 번째 원시 타입을 갖습니다. 이를 파생이라고도 하지만 Memo, Computed, Pure Computed라고도 합니다.
fullName
파생을 만들 때 어떤 일이 발생하는지 비교해보겠습니다.
여기서는 fullName
이 생성과 동시에 값을 계산한 다음 리액션에서 읽을 때에는 표현식을 다시 실행하지 않습니다. 소스 시그널을 업데이트할 때 다시 실행되지만, 해당 변경 사항이 리액션에 전파될 때 한 번만 수행됩니다.
fullName을 계산하는 것은 비용이 많이 들지는 않지만, 독립적으로 실행된 표현식에서 값을 캐싱해 파생이 어떻게 계산을 절약하는지에 대해 알 수 있습니다. 또한 이 값은 자체적으로 추적이 가능합니다.
또한, 파생될 때 동기화가 보장됩니다. 언제든지 의존성을 확인하고 오래된(stale) 상태인지를 평가할 수 있습니다. 리액션을 사용해 다른 시그널에 작성하는 것은 동등해 보일 수 있지만 보장되지는 않습니다. 이런 리액션은 시그널의 명시적 의존성이 아닙니다(시그널에는 의존성이 없기 때문입니다). 다음 문단에서 종속성의 개념에 대해 더 자세히 살펴보겠습니다.
참고: 일부 라이브러리는 파생 값을 읽을 때만 계산하면 되는 지연 평가를 사용하므로 현재 읽지 않는 파생 값을 적극적으로 폐기할 수 있습니다. 이 글의 범위를 벗어나는 이런 접근 방식에는 트레이드오프가 있습니다.
반응형 생명주기
세분화된 반응형은 많은 반응형 노드 간의 연결을 유지합니다. 주어진 변경사항에 대해 그래프의 일부는 재평가되고 연결을 생성 및 제거할 수 있습니다.
참고: Svelte 또는 Marko 같은 사전 컴파일을 하는 라이브러리는 동일한 런타임 추적 기술을 사용하지 않고 정적으로 의존성을 분석합니다. 따라서 반응형 표현식이 다시 실행되는 시기에 대한 제어가 미흡해 초과 실행될 수 있지만, 구독 관리에 대한 비용이 적습니다.
조건이 값을 파생하는데 사용하는 데이터를 변경하는 경우를 살펴보겠습니다.
주목해야 할 점은 3단계에서 lastName
을 변경할 때 새 로그를 얻지 못한다는 점입니다. 왜냐하면 반응형 표현식을 다시 실행할 때마다 의존성을 다시 작성하기 때문입니다. 간단히 말해서, lastName
을 변경할 때 아무도 그것을 듣고 있지 않습니다.
showFullName
을 다시 true
로 설정할 때 관찰한 대로 값이 변경됩니다. 그러나 아무것도 통지(notify)되지 않습니다. lastName
이 다시 추적되기 위해서는 showFullName
이 변경 되어야하기 때문에 이는 안전한 상호작용입니다.
의존성은 반응형 표현식이 값을 생성하기 위해 읽는 신호입니다. 차례로, 이런 시그널은 많은 반응형 표현식 구독을 갖습니다. 업데이트할 때 의존하고 있는 구독자들에게 통지됩니다.
우리는 각 실행에 대해 이런 구독/의존성을 구성합니다. 그리고 반응형 표현식이 재실행될 때 또는 최종적으로 해제될 때 이 구성을 해제합니다. onCleanup
헬퍼 함수를 통해 해당 시점을 확인할 수 있습니다.
동기 실행
세분화된 반응형 시스템은 변경 사항을 동기적으로 즉시 실행합니다. 일관성 없는 상태를 관찰할 수 없다는 점에서 결함이 없는 것을 목표로 합니다. 이는 주어진 변경된 코드에서 한 번만 실행되기 때문에 예측 가능성으로 이어집니다.
일관성 없는 상태는, 우리가 관찰한 것을 믿고 결정을 내리고 작업을 수행할 수 없을 때 의도하지 않는 동작으로 이어질 수 있습니다.
이런 동작이 어떻게 실행되는지 보여주는 가장 쉬운 방법은 리액션을 실행하는 파생 값에 공급되는 두 가지 변경 사항을 동시에 적용하는 것입니다. 저희는 여기서 batch 헬퍼 함수를 사용해 시연하겠습니다. batch 함수는 표현식 실행이 완료될 때만 변경 사항을 적용하는 트랜잭션에서 업데이트를 래핑하겠습니다.
이 예시에서 코드는 예상대로 생성된대로 하향식으로 실행됩니다. 그러나 배치 업데이트는 실행/읽기 로그를 되돌립니다.
A와 B가 동시에 적용되어도 값을 업데이트할 때, 저희는 어디선가 A의 의존성을 먼저 실행해야 합니다. 따라서 effect가 먼저 실행되지만, C가 오래되었다(stale)는 것을 감지하면 즉시 읽기에서 실행되며 모든 것이 한번 실행되고 올바르게 평가됩니다.
물론 이 정적인 경우를 순서대로 해결하는 방법을 생각할 수 있지만, 의존성은 모든 실행에서 변경될 수 있습니다. 세분화된 반응형 라이브러리는 하이브리드 푸시/풀 방식을 사용해 일관성을 유지합니다. 이벤트/스트림과 같이 순전히 “푸시”하지 않고 제너레이터같이 순전히 “풀”하지도 않습니다.
결론
이 글에서 많은 것을 다뤘습니다. 핵심 원시 타입을 소개하고 의존성 해결 및 동기 실행을 포함해 세분화된 반응형의 특성을 다뤘습니다.
주제가 아직 완전히 명확하지 않은 것 같아도 괜찮습니다. 이 글을 읽고 예제를 엉망으로 만들어보세요. 이 글은 가장 최소한의 방법으로 아이디어를 나타내기 위해 작성했습니다. 하지만, 이 내용이 실제로 대부분을 차지합니다. 약간의 연습을 통해 데이터를 세부적으로 모델링하는 방법을 알 수 있을 것입니다.
🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article(https://kofearticle.substack.com/)을 구독해주세요!