SolidJS와 ν•¨κ»˜ λ˜μ§šμ–΄λ³΄λŠ” λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°


λ“€μ–΄κ°€λ©°

졜근 λͺ‡ λ…„ 사이 λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°(Reactive Programming)μ΄λΌλŠ” κ°œλ…μ΄ μ›Ή ν”„λŸ°νŠΈμ—”λ“œ 개발 뢄야에 많이 μŠ€λ©°λ“€μ—ˆλ‹€. Vue.jsκ°€ λŒ€ν‘œμ μœΌλ‘œ λ°˜μ‘ν˜• ν”„λ ˆμž„μ›Œν¬λ‘œ μ•Œλ €μ ΈμžˆμœΌλ©°, μ΅œκ·Όμ—λŠ” Svelteκ°€ 많이 μ–ΈκΈ‰λ˜κ³  μžˆλ‹€. AngularλŠ” λŒ€ν‘œμ μΈ λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ° 라이브러리 RxJSλ₯Ό ν”Όμ–΄ μ˜μ‘΄μ„±(Peer Dependency)으둜 두고 있으며, 이λ₯Ό ν™œμš©ν•˜μ—¬ λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ΄ κ°€λŠ₯ν•˜λ„λ‘ μ„€κ³„λ˜μ–΄μžˆλ‹€.

그리고 Reactλ₯Ό λΉΌλ†“μœΌλ©΄ μ„­μ„­ν•œλ°, ReactλŠ” μ—„λ°€νžˆ 말해 λ°˜μ‘ν˜• λΌμ΄λΈŒλŸ¬λ¦¬κ°€ μ•„λ‹ˆλ‹€. λ‹€λ§Œ React와 ν•¨κ»˜ 많이 ν™œμš©ν•˜κ³  μžˆλŠ” μƒνƒœ 관리 라이브러리 쀑 MobXκ°€ λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ΄ 적용된 라이브러리둜 잘 μ•Œλ €μ Έ μžˆλ‹€. (React의 λ°˜μ‘μ„±(Reactivity)에 λŒ€ν•œ μ£Όμ œλŠ” 이번 κΈ€μ—μ„œ μ΄μ•ΌκΈ°ν•˜κ³ μž ν•˜λŠ” λ‚΄μš©μ„ λ²—μ–΄λ‚˜κΈ° λ•Œλ¬Έμ— 깊게 μ–ΈκΈ‰ν•˜μ§€ μ•ŠλŠ”λ‹€.)

"νŒ€ μ•ˆμ—μ„œλŠ” Reactκ°€ μ™„μ „νžˆ 'λ°˜μ‘ν˜•'이 되렀 ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— 'Schedule'둜 λΆˆλ €μ•Ό ν•œλ‹€λŠ” 농담을 ν•©λ‹ˆλ‹€."(There is an internal joke in the team that React should have been called β€œSchedule” because React does not want to be fully β€œreactive”.) -- https://reactjs.org/docs/design-principles.html#scheduling

λ°˜μ‘ν˜• 라이브러리 쀑 아직 μ–ΈκΈ‰ν•˜μ§€ μ•Šμ€ 것이 μžˆμœΌλ‹ˆ, λ°”λ‘œ 였늘 이야기할 SolidJS이닀. 이 λΌμ΄λΈŒλŸ¬λ¦¬λŠ” 개발이 μ‹œμž‘λœ 지 5년이 λ„˜μ—ˆμ§€λ§Œ, 2021λ…„ State of JS μ„€λ¬Έμ—μ„œμ•Ό 처음 μˆœμœ„ λͺ©λ‘μ— λͺ¨μŠ΅μ„ λ“œλŸ¬λƒˆλ‹€. 그리고 섀문에 μ°Έμ—¬ν•œ μ‚¬λžŒλ“€μ˜ λ§Œμ‘±λ„ μˆœμœ„ 쀑 1μœ„λ₯Ό λ‹¬μ„±ν–ˆλ‹€. ν•„μžλŠ” SolidJS νŠΉμ§•κ³Ό 이 λΌμ΄λΈŒλŸ¬λ¦¬κ°€ μΆ”κ΅¬ν•˜λŠ” λ°˜μ‘ν˜• λͺ¨λΈμ„ μ‚΄νŽ΄λ³΄λ©΄μ„œ, λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ˜ κ°œλ…μ„ λ‹€μ‹œ λŒμ•„λ³΄κ²Œ λ˜μ—ˆλ‹€.

이번 글을 톡해 λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ˜ κ°œλ…μ„ κ°€λ³κ²Œ μ‚΄νŽ΄λ³Έ ν›„ κ΅¬ν˜„ λ°©μ‹μ˜ ν•œ ν˜•νƒœμΈ '투λͺ…ν•œ λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°(Transparent Reactive Programming, TRP)'을 μ•Œμ•„λ³Έλ‹€. 그리고 투λͺ…ν•œ λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ˜ κ°œλ…μ΄ 적용된 SolidJS의 λ°˜μ‘ν˜• λͺ¨λΈκ³Ό SolidJS의 λ Œλ”λ§ νŠΉμ§•μ„ κ°„λ‹¨νžˆ μ†Œκ°œν•œλ‹€.

λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°?

λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ„ μ„€λͺ…ν•˜κ³ μž ν•˜λŠ” μžλ£ŒλŠ” 무척 λ§Žμ§€λ§Œ μ‰½κ²Œ μ™€λ‹ΏλŠ” μžλ£ŒλŠ” κ·Έλ ‡κ²Œ λ§Žμ§€ μ•Šμ•˜λ‹€. κ·Έλ‚˜λ§ˆ 짧게 μš”μ•½ν•˜μžλ©΄ μ•„λž˜μ™€ 같이 ν‘œν˜„ν•  수 μžˆμ„ 것이닀.

"λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ€ 데이터 μ€‘μ‹¬μ˜ 이벀트 이미터 μœ„μ— λ§Œλ“€μ–΄μ§„ 선언적 ν”„λ‘œκ·Έλž˜λ° νŒ¨λŸ¬λ‹€μž„μ΄λ‹€.(Reactive Programming is a declarative programming paradigm built on data-centric event emitters.)" -- Ryan Carniato(SolidJS의 μ œμž‘μž)

μ—¬κΈ°μ„œ "선언적 ν”„λ‘œκ·Έλž˜λ° νŒ¨λŸ¬λ‹€μž„"κ³Ό "데이터 μ€‘μ‹¬μ˜ 이벀트 이미터" λΌλŠ” 두 가지 ν¬μΈνŠΈκ°€ μžˆλ‹€. κ°„λ‹¨ν•˜κ²Œ ν•˜λ‚˜μ”© μ§šμ–΄λ³΄λ©΄

  • 선언적 ν”„λ‘œκ·Έλž˜λ° νŒ¨λŸ¬λ‹€μž„: μ½”λ“œκ°€ 과정을 ν‘œν˜„ν•˜λŠ” 것이 μ•„λ‹ˆλΌ ν–‰μœ„ 자체λ₯Ό ν‘œν˜„ν•˜λŠ” 것. λŒ€ν‘œμ μΈ 예둜 HTMLμ΄λ‚˜ SQL같이 μ–΄λ–»κ²Œ λ‚΄λΆ€ ꡬ쑰가 이루어져 DOM을 λ Œλ”λ§ν•˜κ±°λ‚˜ 데이터λ₯Ό κ°€μ Έμ˜¬μ§€ ν‘œν˜„ν•˜λŠ” 것이 μ•„λ‹ˆλΌ μ–΄λ–€ DOM을 ν‘œν˜„ν• μ§€, μ–΄λ–€ 데이터λ₯Ό κ°€μ Έμ˜¬ μ§€λ§Œ ν‘œν˜„ν•˜κ³  μžˆλ‹€.
  • 데이터 μ€‘μ‹¬μ˜ 이벀트 이미터: μš°λ¦¬λŠ” μ΄λ²€νŠΈκ°€ μ‘΄μž¬ν•˜λŠ” μ‹œμŠ€ν…œ μœ„μ—μ„œ κ°œλ°œμ„ ν•˜κ³  μžˆλ‹€. DOM 뿐 μ•„λ‹ˆλΌ OS도 이벀트 큐λ₯Ό 가지고 μžˆλ‹€. 덕뢄에 λ³€ν™”λ₯Ό λ‹€λ£¨λŠ” λΆ€λΆ„κ³Ό λ³€ν™”λ₯Ό μ‹€ν–‰ν•˜λŠ” μ•‘ν„°(actors)λ₯Ό λΆ„λ¦¬ν•˜μ—¬ λ‹€λ£° 수 μžˆλ‹€. λ°˜μ‘ν˜• μ‹œμŠ€ν…œμ˜ 핡심은 μ•‘ν„°κ°€ λ°μ΄ν„°λΌλŠ” 것이닀. 각각의 데이터가 값이 λ°”λ€Œμ—ˆμ„ λ•Œ 값이 λ³€κ²½λ˜μ—ˆλ‹€λŠ” 이벀트λ₯Ό μ‹€ν–‰ν•˜κ³  κ΅¬λ…μžμ—κ²Œ μ•Œλ¦¬λŠ” μ±…μž„μ„ 가지고 μžˆλ‹€.

μœ„μ˜ μš”μ•½μ— 따라 λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ΄ 적용된 λŒ€ν‘œμ μΈ 예λ₯Ό μ‚΄νŽ΄λ³΄μž. λ°”λ‘œ μŠ€ν”„λ ˆλ“œμ‹œνŠΈμ΄λ‹€.


좜처: What is Functional Reactive Programming (FRP)?

A2셀은 κ·Έμ € "B2μ…€μ˜ κ°’κ³Ό C2μ…€μ˜ 합을 ν‘œν˜„ν•œλ‹€"λΌλŠ” 선언적인 ν‘œν˜„λ§Œ μž‘μ„±λ˜μ–΄μžˆμ„ 뿐이닀. μš°λ¦¬κ°€ μ–΄λ–»κ²Œ B2 μ…€κ³Ό C2 μ…€μ—μ„œ 값을 꺼내와 계산할지 등을 λͺ…μ‹œμ μœΌλ‘œ ν‘œν˜„ν•˜μ§€ μ•Šμ•˜λ‹€. B2λ‚˜ C2μ…€μ˜ 변경사항은 μžλ™μœΌλ‘œ μ „νŒŒλ˜μ–΄ A2의 값에 반영될 것이닀.

더 λ‚˜μ•„κ°€ 'μ΄λ²€νŠΈλ‘œλΆ€ν„° μ‹œμž‘ν•˜μ—¬ μ‹œκ°„μ— 따라 λ³€ν•˜λŠ” κ°’μ˜ 관계λ₯Ό μ„ μ–Έμ μœΌλ‘œ ν‘œν˜„ν•œλ‹€' λŠ” κ°œλ…μ„ κ΅¬ν˜„ν•œ λŒ€ν‘œμ μΈ κ΅¬ν˜„μ²΄κ°€ ReactiveX(Rx)이닀. RxλŠ” 이벀트λ₯Ό 비동기 슀트림으둜 λ°”κΎΈκ³ , λ‹€μ–‘ν•œ μ˜€νΌλ ˆμ΄ν„°λ₯Ό μ μš©ν•˜λ©° 이 슀트림으둜 무엇을 ν•˜κ³ μž ν•˜λŠ”μ§€ μ„ μ–Έμ μœΌλ‘œ ν‘œν˜„ν•œλ‹€.


좜처: The introduction to Reactive Programming you've been missing

μœ„μ˜ 이미지와 같이 더블 클릭 μ΄μƒμ˜ 이벀트λ₯Ό μˆ˜μ‹ ν•˜λŠ” 흐름을 λͺ‡ μ€„μ˜ μ½”λ“œλ‘œ, 그리고 μ„ μ–Έμ μœΌλ‘œ λ§Œλ“€μ–΄λ‚Ό 수 μžˆλ‹€. 그런데 μ™œ μš°λ¦¬κ°€ 이런 λ°©μ‹μ˜ λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ„ μ•Œ ν•„μš”κ°€ μžˆμ„κΉŒ? λ°”λ‘œ UIλ₯Ό κ΅¬μ„±ν•˜λŠ”λ° μœ μš©ν•˜κΈ° λ•Œλ¬Έμ΄λ‹€.

μš°λ¦¬κ°€ μ‚¬μš©μžμ—κ²Œ μ œκ³΅ν•˜λŠ” UIλŠ” μ–Έμ œ 일어날 지 μ•Œ 수 μ—†λŠ” κ±°λŒ€ν•œ 이벀트 트리거 λ­‰μΉ˜λ‚˜ 닀름없닀. λ‹€μ–‘ν•œ μƒν˜Έμž‘μš©μ΄ μ‹€μ‹œκ°„μœΌλ‘œ μΌμ–΄λ‚˜λ©΄μ„œ κ·Έ μƒν˜Έμž‘μš©μ˜ κ²°κ³Όλ₯Ό ν‘œμ‹œν•˜λŠ” λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μž‘μ„±ν•  λ•Œ, 효과적으둜 이벀트λ₯Ό μ²˜λ¦¬ν•˜λŠ” μ½”λ“œλ₯Ό 가독성 있게 μ μž¬μ μ†Œμ— μž‘μ„±ν•˜κΈ°λŠ” 쉽지 μ•Šμ€ 일이닀. ν•˜μ§€λ§Œ λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ„ 톡해 μ½”λ“œμ˜ 좔상화 단계λ₯Ό λŒμ–΄μ˜¬λ € κ΅¬ν˜„ 방법 κ·Έ 자체λ₯Ό κ³ λ―Όν•˜λŠ” 것보닀 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ„ μ–Έμ μœΌλ‘œ ν‘œν˜„ν•˜λŠ” 것에 더 집쀑할 수 있게 λœλ‹€.

ν•˜μ§€λ§Œ 슀트림 기반의 λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ€ λŸ¬λ‹ μ»€λΈŒκ°€ λ†’κ³ , λ¬Έμ œκ°€ λ°œμƒν–ˆμ„ λ•Œ 디버깅이 μ–΄λ ΅λ‹€λŠ” 단점이 자주 μ–ΈκΈ‰λ˜μ–΄μ™”λ‹€. 슀트림 기반의 λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž¨ 외에 많이 μ•Œλ €μ§„ ν˜•νƒœμ˜ λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ° 방식이 λ°”λ‘œ μ΄λ²ˆμ— 이야기할 Transparent Reactive Programming(TRP)이닀.

'Transparent' λŠ” 투λͺ…ν•œ, ν˜Ήμ€ λͺ…μΎŒν•œμ΄λž€ 뜻으둜 λ³„λ„μ˜ μ½”λ“œ λ³€κ²½ 없이 λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°μ΄ μ΄λ£¨μ–΄μ§€λŠ” κ²ƒμ΄λΌλŠ” μ˜λ―Έλ‹€. 슀트림과 μ˜€νΌλ ˆμ΄ν„° λŒ€μ‹  μžλ™μœΌλ‘œ 좔적 계산이 κ°€λŠ₯ν•œ 데이터λ₯Ό 기반으둜 이루어져 있으며, λ°μ΄ν„°μ˜ λ³€κ²½ 사항을 μ „νŒŒν•˜μ—¬ νŒŒμƒλœ 값을 ν˜•μ„±ν•˜κ³  ꢁ극적으둜 λΆ€μˆ˜ νš¨κ³Όλ„ μΌμœΌν‚¨λ‹€.

이런 방식을 μ°¨μš©ν•œ 라이브러리 ν˜Ήμ€ ν”„λ ˆμž„μ›Œν¬λ‘œ MobX, Vue.js, SolidJS, Svelte 등이 μžˆλ‹€. TRPλŠ” 슀트림 기반의 λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°λ³΄λ‹€ 비ꡐ적 λ‹¨μˆœν•˜κ²Œ λ™μž‘ν•˜λ©΄μ„œ, 잠재적으둜 μ‘΄μž¬ν•˜λŠ” λΆˆμ•ˆ μš”μ†Œλ„ μ œμ–΄ν•  수 μžˆλ‹€.

  • 쀑볡 ꡬ독을 ν•˜κ±°λ‚˜, ν•„μš”ν•œ 곳에 ꡬ독이 μ œλŒ€λ‘œ λ˜μ§€ μ•Šμ€ 경우
  • μžλ™μœΌλ‘œ ꡬ독이 ν•΄μ œλ˜μ—ˆμœΌλ©΄ ν•˜λŠ” 경우

μ–΄λ–€ λ©΄μ—μ„œ TRPλŠ” 비ꡐ적 λ‹¨μˆœν•˜κ²Œ, 그리고 투λͺ…ν•˜κ²Œ λ™μž‘ν•œλ‹€κ³  λ³Ό 수 μžˆμ„κΉŒ? TRP의 κΈ°λ³Έ ꡬ성 μš”μ†Œλ₯Ό μ‚΄νŽ΄λ³΄λ©° μ‹€λ§ˆλ¦¬λ₯Ό μ°Ύμ•„λ³΄μž.

TRP의 κΈ°λ³Έ ꡬ성 μš”μ†Œ

TRP의 Primitive(일반적인 ν”„λ‘œκ·Έλž˜λ° μ–Έμ–΄μ˜ μ›μ‹œ νƒ€μž…κ³Ό μœ μ‚¬ν•˜μ§€λ§Œ, λ™μΌν•œ κ°œλ…μ€ μ•„λ‹ˆλΌκ³  νŒλ‹¨ν•˜μ—¬ 이 κΈ€μ—μ„œλŠ” κΈ°λ³Έ ꡬ성 μš”μ†ŒλΌλŠ” μš©μ–΄λ₯Ό μ‚¬μš©ν•œλ‹€.)λŠ” 크게 두 가지가 μžˆλ‹€. Observable(μ˜΅μ €λ²„λΈ”), 그리고 Reaction(λ°˜μ‘)이닀.

Observable(μ˜΅μ €λ²„λΈ”)

λ¨Όμ € μ˜΅μ €λ²„λΈ”μ€ κ΄€μΈ‘ κ°€λŠ₯ν•œ κ°’μœΌλ‘œμ„œ κ·Έ 값을 μ‘°νšŒν•˜λŠ” Subscriber(κ΅¬λ…μž)λ₯Ό κ΄€λ¦¬ν•˜κ³ , 값이 변경될 λ•Œ λ³€κ²½ 사항을 κ΅¬λ…μžμ—κ²Œ μ „νŒŒν•˜λŠ” 역할을 ν•œλ‹€. λ‹€μ–‘ν•œ λΌμ΄λΈŒλŸ¬λ¦¬μ—μ„œ Atom, Signal, Ref λ“±μ˜ 각기 λ‹€λ₯Έ μš©μ–΄λ₯Ό μ‚¬μš©ν•˜μ§€λ§Œ κ°œλ…μ€ λ™μΌν•˜λ‹€. μžλ°”μŠ€ν¬λ¦½νŠΈλ‘œ μ˜΅μ €λ²„λΈ”μ„ κ΅¬ν˜„ν•  λ•Œ ES6 Proxyλ₯Ό μ‚¬μš©ν•˜κ±°λ‚˜ 직접 Object.defineProperty μ‚¬μš©ν•˜μ—¬ 객체의 속성을 μ˜€λ²„λΌμ΄λ“œν•˜λŠ” λ°©μ‹μœΌλ‘œ κ΅¬ν˜„ν•œλ‹€. μžμ„Έν•œ κ΅¬ν˜„ μ›λ¦¬λŠ” μ•„λž˜μ˜ 글을 μ°Έκ³ ν•˜λΌ.

μ•„λž˜λŠ” Proxy 기반의 μ˜΅μ €λ²„λΈ”μ„ κ΅¬ν˜„ν•˜λŠ” μ½”λ“œ 일뢀이닀. λŒ€ν‘œμ μœΌλ‘œ MobXλŠ” 버전 5λΆ€ν„°, VueλŠ” 버전 3λΆ€ν„° Proxyλ₯Ό μ‚¬μš©ν•˜μ—¬ μ˜΅μ €λ²„λΈ”μ„ κ΅¬ν˜„ν•˜κ³  μžˆλ‹€.

function observable(data) {
  const handler = {
    get: function(target, key, receiver) {
      // κ΅¬λ…μžλ₯Ό λ“±λ‘ν•œλ‹€.
      // ...
      return Reflect.get(target, key, receiver);
    },
    set: function(target, key, value, receiver) {
      // κ°’μ˜ 변경을 κ΅¬λ…μžμ—κ²Œ μ•Œλ¦°λ‹€.
      // ...
      return Reflect.set(target, key, value, receiver);
    }
  };
  
  return new Proxy(data, handler);
}

Reaction(λ°˜μ‘)

λ°˜μ‘μ€ μ˜΅μ €λ²„λΈ” 값이 변화될 λ•Œ νŠΉμ • λ™μž‘μ„ μˆ˜ν–‰ν•˜λŠ” 것이며 두 가지 ν˜•νƒœλ‘œ λ‚˜λ‰œλ‹€. μ˜΅μ €λ²„λΈ” 값을 기반으둜 νŒŒμƒλœ 값을 λ¦¬ν„΄ν•˜λŠ” Derivation(νŒŒμƒ, ν˜Ήμ€ Computed)κ³Ό, κ΄€μ°°ν•˜κ³  μžˆλŠ” 값이 변경될 λ•Œλ§ˆλ‹€ μ‚¬μ΄λ“œ μ΄νŽ™νŠΈλ₯Ό μ‹€ν–‰ν•˜λŠ” Effect(μ΄νŽ™νŠΈ)이닀.

μ΄νŽ™νŠΈλŠ” ν•¨μˆ˜λ₯Ό 인자둜 λ°›μ•„ κ·Έ ν•¨μˆ˜ μ•ˆμ—μ„œ μ‚¬μš©λ˜λŠ” μ˜΅μ €λ²„λΈ” κ°’μ˜ λ³€ν™”κ°€ λ°œμƒν•  λ•Œλ§ˆλ‹€ 인자둜 받은 ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•˜λŠ” 것이닀. μ˜΅μ €λ²„λΈ” 값이 μžˆλ‹€ ν•˜λ”λΌλ„ κ·Έ 값이 변경될 λ•Œλ§ˆλ‹€ 무엇을 ν•΄μ•Ό 할지 맀번 μ½”λ“œλ₯Ό μž‘μ„±ν•΄μ£ΌλŠ” 것이 μ•„λ‹ˆλΌ, λ³€ν™”κ°€ λ°œμƒν•  λ•Œλ§ˆλ‹€ μžλ™μœΌλ‘œ μ‹€ν–‰λ˜λŠ” λΆ€μˆ˜ 효과λ₯Ό 톡해 UI의 λ™μž‘μ„ 효과적으둜 관리할 수 있게 λœλ‹€. MobX의 autorun, Vue의 watch 등이 λŒ€ν‘œμ μΈ μ˜ˆμ΄λ‹€.

// observable 값을 톡해 document.title을 λ³€κ²½ν•˜λŠ” 예.
const user = observable({ username: 'iamironman', fullname: 'Tony Stark' });

effect(() => {
  // μ΄νŽ™νŠΈλŠ” μ΅œμ΄ˆμ— ν•œ 번 μ‹€ν–‰λ˜κ³ , 이후 user.fullname 값이 변화될 λ•Œλ§ˆλ‹€ μžλ™μœΌλ‘œ λ‹€μ‹œ ν˜ΈμΆœλœλ‹€.
  document.title = user.fullname;
});

user.fullname = 'Riri Williams';
// 타이틀 변경됨

React에 μ΅μˆ™ν•˜λ‹€λ©΄ useEffect 훅을 ν™œμš©ν•œ μ½”λ“œμ™€ λΉ„μŠ·ν•œ ν˜•νƒœλ‘œ 보일 수 μžˆλ‹€. useEffect ν›…κ³Ό 달리 TRP의 λ°˜μ‘μ€ μ–΄λ–€ μ˜΅μ €λ²„λΈ” 값을 μΆ”μ ν•˜κ³  μžˆλŠ”μ§€ μžλ™μœΌλ‘œ κ΄€λ¦¬ν•œλ‹€. 이λ₯Ό μœ„ν•΄ μ΄νŽ™νŠΈλŠ” 맀번 κ΄€μ°° 쀑인 μ˜΅μ €λ²„λΈ” 값이 λ°”λ€” λ•Œλ§ˆλ‹€ μžμ‹ μ΄ μ–΄λ–€ 값에 μ˜μ‘΄ν•˜κ³  μžˆλŠ”μ§€ μž¬ν‰κ°€ν•˜λŠ” 과정을 κ±°μΉœλ‹€. 이 λ™μž‘μ„ 기반으둜 졜근의 λ°˜μ‘ν˜• λΌμ΄λΈŒλŸ¬λ¦¬λŠ” 'μžλ™μœΌλ‘œ μ˜μ‘΄μ„±μ„ νŒŒμ•…ν•˜λŠ”(Automatic Dependency Detection)' 단계에 이λ₯Ό 수 있게 λ˜μ—ˆλ‹€.

λ§ˆμ§€λ§‰μœΌλ‘œ νŒŒμƒμ€ 기본적으둜 μ΄νŽ™νŠΈμ™€ λ™μΌν•œ λ™μž‘μ„ ν•˜μ§€λ§Œ 값을 λ¦¬ν„΄ν•œλ‹€λŠ” νŠΉμ§•μ΄ μžˆλ‹€. 거기에 결과값을 λ©”λͺ¨μ΄μ œμ΄μ…˜ν•˜μ—¬ λΆˆν•„μš”ν•œ μΆ”κ°€ 계산을 λ°©μ§€ν•˜κ³ , μ‹€μ§ˆμ μœΌλ‘œ 쑰회될 λ•ŒκΉŒμ§€ μ˜΅μ €λ²„λΈ” κ°’μ˜ νŒŒμƒμ΄ μΌμ–΄λ‚˜μ§€ μ•Šλ„λ‘ 지연 평가λ₯Ό ν•  μˆ˜λ„ μžˆλ‹€. λŒ€ν‘œμ μΈ 예둜 MobX의 computedκ°€ μžˆλ‹€.

// μŠ€ν”„λ ˆλ“œμ‹œνŠΈλ₯Ό μƒμƒν•΄λ³΄μž. C1μ—λŠ” A1 * B1μ΄λΌλŠ” ν•¨μˆ˜λ₯Ό μ§€μ •ν•΄λ‘μ—ˆλ‹€.
const A1 = observable({ value: 1 });
const B1 = observable({ value: 15 });

// μ΅œμ΄ˆμ— ν•œ 번 ν‰κ°€λœ λ’€ κ΄€μ°°ν•˜κ³  μžˆλŠ” 값이 변화될 λ•ŒκΉŒμ§€λŠ” μΊμ‹±λœ 값을 λ¦¬ν„΄ν•œλ‹€.
const C1 = computed(() => A1.value * B1.value);
console.log(C1.get()); // 15
console.log(C1.get()); // 15
A1.value = 2;
console.log(C1.get()); // 30

SolidJSλ₯Ό λΉ„λ‘―ν•œ λ‹€μ–‘ν•œ λΌμ΄λΈŒλŸ¬λ¦¬λŠ” TRP κΈ°λ³Έ ꡬ성 μš”μ†Œλ₯Ό 적절히 μ‘°ν•©ν•˜μ—¬ Fine-Grained Reactivity(잘게 λ‚˜λˆ„μ–΄μ§„ λ°˜μ‘μ„±)λ₯Ό κ΅¬μΆ•ν•˜κ³  효과적으둜 값이 λ³€κ²½λœ λΆ€λΆ„λ§Œ μ—…λ°μ΄νŠΈν•˜λŠ” 것을 μΆ”κ΅¬ν•œλ‹€. μ˜΅μ €λ²„λΈ” 값을 ν•˜λ‚˜μ˜ λ…Έλ“œλ‘œ 보고, λ…Έλ“œ μ‚¬μ΄μ˜ 연결을 μ΄˜μ΄˜ν•˜κ²Œ κ΅¬μ„±λœ κ·Έλž˜ν”„λ₯Ό λ§Œλ“€μ–΄ νŠΉμ • 뢀뢄이 λ³€κ²½λ˜λ©΄ μ—°κ΄€λœ λ‹€λ₯Έ λ…Έλ“œλ„ λ°˜μ‘ν•˜μ—¬ λ‹€μ‹œ 값이 ν‰κ°€λ˜λ„λ‘ κ΅¬μ„±ν•˜λŠ” 것이닀.


좜처: Becoming fully reactive: an in-depth explanation of MobX

SolidJS의 λ°˜μ‘ν˜• λͺ¨λΈ

SolidJSλŠ” μœ„μ— μ–ΈκΈ‰ν•œ TRP의 κΈ°λ³Έ κ΅¬μ„±μš”μ†Œλ‘œ λ°˜μ‘ν˜• λͺ¨λΈμ„ κ΅¬μΆ•ν•˜μ˜€λ‹€. μ˜΅μ €λ²„λΈ”μ˜ 역할을 ν•˜λŠ” Signal, λΆ€μˆ˜ 효과λ₯Ό μ²˜λ¦¬ν•˜λŠ” Effect, νŒŒμƒ 값을 λ‹€λ£¨λŠ” Memoκ°€ μžˆλ‹€. 각각 createSignal, createEffect, createMemo λ₯Ό 톡해 λ§Œλ“€μ–΄λ‚Έλ‹€. SolidJSμ—μ„œ μ˜΅μ €λ²„λΈ”μ„ μ œκ³΅ν•˜λŠ” κΈ°λ³Έ API인 createSignal 을 κ°„λ‹¨ν•œ ν˜•νƒœλ‘œ 직접 κ΅¬ν˜„ν•΄λ³΄μž.

const runningContext = [];

function subscribe(running, subscriptions) {
  subscriptions.add(running);
  running.dependencies.add(subscriptions);
}

function createSignal(value) {
  const subscriptions = new Set();
  
  const read = () => {
    const currentRunning = runningContext[runningContext.length - 1];
    if (currentRunning) {
      subscribe(currentRunning, subscriptions);
    }
    return value;
  };
  const write = (nextValue) => {
    value = nextValue;
    
    for (const sub of [...subscriptions]) {
      sub.run();
    }
  };
  
  return [read, write];
}

runningContext λŠ” ν˜„μž¬ 싀행쀑인 λ°˜μ‘μ„ λ‹΄μ•„λ‘λŠ” μŠ€νƒ 역할을 ν•œλ‹€. 그리고 각각의 Signal은 μžμ‹ μ΄ κ΄€λ¦¬ν•˜λŠ” κ΅¬λ…μž 리슀트(subscriptions)κ°€ μžˆλ‹€. 이 두 μžλ£Œκ΅¬μ‘°κ°€ μžλ™μœΌλ‘œ μ˜μ‘΄μ„±μ„ μΆ”μ ν•˜λŠ” κΈ°λŠ₯의 기본이 λœλ‹€. μ˜΅μ €λ²„λΈ” κ°’μ˜ 변화에 따라 λ°˜μ‘μ΄ 일어날 λ•Œ μ‹œκ·Έλ„ μ•ˆμ— μžˆλŠ” read ν•¨μˆ˜κ°€ μ‹€ν–‰λ˜λŠ” 것이고 이 λ•Œ ꡬ독 μ²˜λ¦¬κ°€ λ˜λŠ” 것이닀.

μ—¬κΈ°κΉŒμ§€λ§Œ 보면 subscribe ν•¨μˆ˜ μ•ˆμ—μ„œ ν™œμš©λ˜λŠ” running μ΄λΌλŠ” μΈμžκ°€ 무엇을 μ˜λ―Έν•˜λŠ”μ§€ νŒŒμ•…ν•˜κΈ° μ–΄λ ΅λ‹€. μ‹€μ œλ‘œ μ˜΅μ €λ²„λΈ” 값을 ν™œμš©ν•˜κΈ° μœ„ν•΄ createEffect ν•¨μˆ˜λ„ λ§Œλ“€μ–΄λ³΄μž.

function cleanup(running) {
  for (const dep of running.dependencies) {
    dep.delete(running);
  }
  running.dependencies.clear();
}

function createEffect(fn) {
  const run = () => {
    cleanup(running);
    runningContext.push(running);
    try {
      fn();
    } finally {
      context.pop();
    }
  };
  
  const running = {
    run,
    depdendencies: new Set()
  };
  
  run();
}

λ°˜μ‘μ΄ 싀행될 λ•Œ μžμ‹ μ΄ μ–΄λ–€ 값에 μ˜μ‘΄ν•˜μ—¬ λ°˜μ‘ν•˜κ³  μžˆλŠ”μ§€ κ΄€λ¦¬ν•˜κΈ° μœ„ν•œ dependencies 속성이 λ”°λ‘œ μžˆλ‹€. 그리고 μƒˆλ‘œμ΄ λ°˜μ‘ν• λ•Œλ§ˆλ‹€ 기쑴에 λ“±λ‘ν•΄λ†¨λ˜ ꡬ독을 μ²­μ†Œν•˜κ³  λ‹€μ‹œ κ΅¬λ…ν•˜λŠ” 과정을 κ±°μΉœλ‹€. 덕뢄에 λ™μ μœΌλ‘œ μ˜΅μ €λ²„λΈ” κ°’μ˜ μ˜μ‘΄μ„±μ„ κ΄€λ¦¬ν•˜λŠ” 것이 κ°€λŠ₯ν•΄μ‘Œλ‹€.

νŒŒμƒ 값을 λ¦¬ν„΄ν•˜λŠ” createMemo λŠ” 각쒅 μ΅œμ ν™”λ₯Ό μœ„ν•΄ 더 λ³΅μž‘ν•œ λ°©μ‹μœΌλ‘œ κ΅¬ν˜„λ˜μ–΄μ•Ό ν•˜μ§€λ§Œ, κ°„λž΅ν•œ μ˜ˆμ‹œλ‘œ createSignal, createEffect λ₯Ό ν™œμš©ν•˜μ—¬ λ§Œλ“€ 수 μžˆλ‹€.

function createMemo(fn) {
  const [computed, set] = createSignal();
  createEffect(() => set(fn()));
  return computed;
}

이제 μœ„μ˜ μž¬λ£Œλ“€μ„ μ΄μš©ν•˜μ—¬ μŠ€ν”„λ ˆλ“œμ‹œνŠΈμ™€ μœ μ‚¬ν•œ 예λ₯Ό λ§Œλ“€μ–΄λ³΄μž. νŠΉμ • 쑰건에 따라 κ΄€μ°°ν•˜λŠ” 셀을 λ°”κΎΈμ–΄μ•Ό ν•  λ•Œ μ–΄λ–€ λ°©μ‹μœΌλ‘œ 처리될까?

const [A1, setA1] = createSignal(1);
const [B1, setB1] = createSignal(2);
const [showA1Only, setShowA1Only] = createSignal(false);

const C1 = createMemo(() => showA1Only() ? A1() : A1() + B1());

createEffect(() => console.log('C1: ' + C1()));
// C1: 3
setShowA1Only(true);
// C1: 1
setB1(10);
// 아무 일도 μΌμ–΄λ‚˜μ§€ μ•ŠμŒ
  • μ½˜μ†”μ„ 좜λ ₯ν•˜λŠ” μ΄νŽ™νŠΈλŠ” C1 을 κ΅¬λ…ν•˜κ³  있으며, C1 은 기본적으둜 showA1Only λ₯Ό κ΅¬λ…ν•˜λ©΄μ„œ 상황에 따라 A1 ν˜Ήμ€ A1 κ³Ό B1 λͺ¨λ‘λ₯Ό κ΅¬λ…ν•œλ‹€.
  • λ¨Όμ € μ΄νŽ™νŠΈκ°€ 졜초 μ‹€ν–‰λ˜λ©΄μ„œ C1 을 ν˜ΈμΆœν•˜κ³  κ·Έ κ³Όμ •μ—μ„œ createSignal ν•¨μˆ˜ μ•ˆμ— 있던 read ν•¨μˆ˜κ°€ μ‹€ν–‰λ˜λŠ” μ‹μœΌλ‘œ ꡬ독이 이루어진닀. 이 κ³Όμ •μ—μ„œ showA1Only 도 κ΅¬λ…λ˜κ³ , showA1Only κ°€ false μ΄λ―€λ‘œ A1 κ³Ό B1 을 λͺ¨λ‘ κ΅¬λ…ν•˜μ˜€λ‹€.
  • setShowA1Only 의 값을 λ³€κ²½ν•˜λ©΄μ„œ showA1Only λ₯Ό κ΅¬λ…ν•˜λ˜ C1 의 값이 μž¬κ΅¬μ„±λœλ‹€. λ¨Όμ € 기쑴의 ꡬ독을 λͺ¨λ‘ ν•΄μ œν•˜κ³  λ‹€μ‹œ showA1Only 와 ν˜„μž¬ 쑰건에 λΆ€ν•©ν•˜λŠ” A1 κ°’λ§Œ μƒˆλ‘œ κ΅¬λ…ν•œλ‹€. 이런 λ°©μ‹μœΌλ‘œ 동적 μ˜μ‘΄μ„± 관리가 이루어진닀.
  • 이후 setShowA1Only λ₯Ό false 둜 λ³€κ²½ν•˜μ§€ μ•ŠλŠ” ν•œ 아무리 setB1 을 ν˜ΈμΆœν•΄λ„ μ΄νŽ™νŠΈκ°€ μ‹€ν–‰λ˜μ§€ μ•Šμ„ 것이닀. μ–΄λ””μ—μ„œλ„ B1 을 κ΅¬λ…ν•˜κ³ μžˆμ§€ μ•ŠκΈ° λ•Œλ¬Έμ΄λ‹€.

μ΄λ ‡κ²Œ SolidJS의 λ°˜μ‘ν˜• λͺ¨λΈμ€ 비ꡐ적 μ΄ν•΄ν•˜κΈ° μ‰¬μš°λ©΄μ„œλ„ λ™μ μœΌλ‘œ μ˜μ‘΄μ„± λ³€ν™”λ₯Ό 관리할 수 μžˆμ–΄μ„œ κ°•λ ₯ν•˜κΈ°κΉŒμ§€ ν•˜λ‹€. ν•˜μ§€λ§Œ κ·Έ λŒ€μ‹  μ˜΅μ €λ²„λΈ” 값을 μ‘°νšŒν•˜κ³  κ΄€λ¦¬ν•˜λŠ” μ½”λ“œλ₯Ό μž‘μ„±ν•  λ•Œ createEffect ν˜Ήμ€ createMemo 을 ν™œμš©ν•΄μ•Ό μ›ν•˜λŠ” κ²°κ³Όλ₯Ό 얻을 수 μžˆμ„ 것이닀.

λ˜ν•œ SolidJS의 λ°˜μ‘μ„± λͺ¨λΈμ€ λ³€ν™”λ₯Ό λ™κΈ°μ μœΌλ‘œ(Synchronously) μΆ”μ ν•œλ‹€. μ΄νŽ™νŠΈ μ•ˆμ— setTimeout λ“±μ˜ 비동기 μž‘μ—…μ„ μˆ˜ν–‰ν•˜λŠ” μ½”λ“œκ°€ μžˆλ‹€λ©΄ μ˜΅μ €λ²„λΈ” κ°’μ˜ λ³€ν™”λ₯Ό μ œλŒ€λ‘œ 좔적할 수 μ—†λ‹€. λ¬Όλ‘  이런 문제λ₯Ό λ³΄μ™„ν•˜κΈ° μœ„ν•œ APIκ°€ λ”°λ‘œ 제곡되고 μžˆλ‹€.

SolidJS의 λ Œλ”λ§ νŠΉμ§•

SolidJSλŠ” JSX 문법을 μ œκ³΅ν•˜κΈ΄ ν•˜μ§€λ§Œ React, Vue와 λ‹€λ₯΄κ²Œ Virtual DOM(가상 돔)을 μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€. λŒ€μ‹  Svelte와 μœ μ‚¬ν•˜κ²Œ κ°œλ°œμžκ°€ μž‘μ„±ν•œ JSX와 λ°˜μ‘ν˜• μš”μ†Œλ“€μ„ 기반으둜 μ»΄νŒŒμΌμ„ ν•˜κ³ , λŸ°νƒ€μž„μ—μ„œλŠ” μ‚¬μš©μžμ˜ μΈν„°λž™μ…˜μ— 따라 직접 λ³€ν™”κ°€ μΌμ–΄λ‚˜μ•Ό ν•˜λŠ” λΆ€λΆ„μ˜ DOM만 λ³€κ²½ν•˜λŠ” μ „λž΅μ„ μ‚¬μš©ν•˜κ³  μžˆλ‹€.

React의 μ»΄ν¬λ„ŒνŠΈκ°€ μƒνƒœ λ³€ν™” 등에 따라 render(ν•¨μˆ˜ μ»΄ν¬λ„ŒνŠΈλŠ” JSX 리턴 ꡬ문)λ₯Ό 계속 ν˜ΈμΆœν•˜λŠ” 것과 달리 SolidJS의 μ»΄ν¬λ„ŒνŠΈλŠ” ν•œ 번 μ‹€ν–‰λ˜λ©΄ 끝인 μƒμ„±μžμ— 가깝닀.

// μΈν„°λ²Œμ— 따라 μΉ΄μš΄ν„°κ°€ λ³€κ²½λ˜λŠ” 예
import { createSignal, onCleanup } from "solid-js";
import { render } from "solid-js/web";

const CountingComponent = () => {
  const [count, setCount] = createSignal(0);
  const interval = setInterval(
    () => setCount(c => c + 1),
    1000
  );
  onCleanup(() => clearInterval(interval));
  return <div>Count value is {count()}</div>;
};

render(() => <CountingComponent />, document.getElementById("app"));

React라면 count κ°€ 변화될 λ•Œλ§ˆλ‹€ μ»΄ν¬λ„ŒνŠΈκ°€ λ‹€μ‹œ 호좜되고 λ¦¬ν„΄λ˜λŠ” 가상 λ”μ˜ λ³€ν™”λ₯Ό λΉ„κ΅ν•˜μ—¬ λΈŒλΌμš°μ €μ—μ„œ κ·Έλ¦¬λŠ” μž‘μ—…μ„ μˆ˜ν–‰ν•  것이닀. 그러면 μ»΄ν¬λ„ŒνŠΈκ°€ ν˜ΈμΆœλ˜λŠ” λ™μ•ˆ μΈν„°λ²Œμ„ κ΄€λ¦¬ν•˜κΈ° μœ„ν•΄ useEffect λ“±μ˜ 훅을 μ‚¬μš©ν•˜μ—¬ 관리해 μ£Όμ–΄μ•Ό ν•œλ‹€.

DOM을 직접 λ³€κ²½ν•˜λŠ” 만큼 μ΅œμ ν™”λ₯Ό μœ„ν•΄ λ‹€λ₯Έ μ»΄ν¬λ„ŒνŠΈμ— μ „λ‹¬λ˜λŠ” props도 ν”„λ‘μ‹œ 객체둜 감싸진 λ’€ μ΅œλŒ€ν•œ 값이 ν•„μš”ν•œ μˆœκ°„κΉŒμ§€ λ―Έλ£¨μ—ˆλ‹€κ°€ 지연 ν‰κ°€λœλ‹€. λ”°λΌμ„œ React μ»΄ν¬λ„ŒνŠΈμ—μ„œ propsλ₯Ό κ°€μ Έλ‹€ μ“Έ λ•Œ ν•˜λ˜ κ²ƒμ²˜λŸΌ ꡬ쑰 λΆ„ν•΄ ν• λ‹Ήν•˜λŠ” 경우 μ»΄ν¬λ„ŒνŠΈκ°€ μ œλŒ€λ‘œ λ°˜μ‘ν•˜μ§€ μ•ŠλŠ”λ‹€κ³  ν•œλ‹€.

const Parent = () => {
  const [greeting, setGreeting] = createSignal("Hello");

  return (
    <section>
      <Label greeting={greeting()}>
        <div>John</div>
      </Label>
    </section>
  );
};

// greeting, children의 변화에 λ°˜μ‘ν•œλ‹€.
// ({greeting, children}) => {...} 같은 μ‹μœΌλ‘œ μ„ μ–Έν•˜λ©΄ λ°˜μ‘ν•˜μ§€ μ•ŠλŠ”λ‹€.
const Label = (props) => (
  <>
    <div>{props.greeting}</div>
    {props.children}
  </>
);

마치며

슀트림 기반의 λ°˜μ‘ν˜• ν”„λ‘œκ·Έλž˜λ°λ³΄λ‹€ 쑰금 더 μΉœμˆ™ν•˜κ²Œ λ‹€κ°€μ˜€λŠ” TRPλ₯Ό 톡해 μ–΄λ–»κ²Œ μš”μ¦˜ λ°˜μ‘ν˜• λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ΄ λ°˜μ‘ν˜• μ‹œμŠ€ν…œμ„ κ΅¬ν˜„ν•˜λŠ”μ§€ 쑰금 더 μžμ„Ένžˆ μ•Œμ•„λ³Ό 수 μžˆμ—ˆκ³ , κ·Έ μ˜ˆμ‹œ 쀑 ν•˜λ‚˜λ‘œ SolidJSλ₯Ό μ‚΄νŽ΄λ³΄μ•˜λ‹€.

μƒˆλ‘œμš΄ 라이브러리(ν˜Ήμ€ ν”„λ ˆμž„μ›Œν¬)의 ν™μˆ˜ μ†μ—μ„œ κ°œλ°œμžλŠ” 'λ‚΄κ°€ λ§Œλ“œλŠ”(ν˜Ήμ€ λ§Œλ“€κ³ μž ν•˜λŠ”) ν”„λ‘œκ·Έλž¨μ˜ μš©λ„μ— λ§žλŠ”μ§€', 'μ–Όλ§ˆλ‚˜ 효율적으둜 ν”„λ‘œκ·Έλž¨μ„ μž‘μ„±ν•  수 μžˆλŠ”μ§€' λ“±μ˜ λͺ…ν™•ν•œ 기쀀을 가지고 라이브러리λ₯Ό μ‚΄νŽ΄λ³΄μ•„μ•Ό ν•  것이닀. 덀으둜 라이브러리의 κ·Όλ³Έ μ›λ¦¬λ‚˜ λ””μžμΈ 철학을 νŒŒμ•…ν•  수 μžˆλ‹€λ©΄ λ”μš± 쒋은 선택을 ν•  수 μžˆμ„ 것이닀.

SolidJSλŠ” κ·Έ 원리와 ꡬ성을 μƒμ„Έν•˜κ²Œ λ¬Έμ„œν™”ν•΄λ‘μ—ˆλ‹€. 이번 μœ„ν΄λ¦¬λ₯Ό μž‘μ„±ν•˜κΈ° μœ„ν•΄ 자료λ₯Ό μ‘°μ‚¬ν•˜κ³  μ •λ¦¬ν•˜λŠ” κ³Όμ •μ—μ„œ μ œμž‘μžμ˜ 식견에 크게 κ°νƒ„ν•˜κΈ°λ„ ν–ˆλ‹€. ν•œ 번 SolidJS의 곡식 λ¬Έμ„œλ₯Ό ν›‘μ–΄λ³΄λ©΄μ„œ μš”μ¦˜ ν”„λŸ°νŠΈμ—”λ“œ μ˜μ—­μ˜ λ°˜μ‘ν˜• νŒ¨λŸ¬λ‹€μž„μ€ μ–΄λ–€ λ°©ν–₯으둜 ν˜λŸ¬κ°€κ³  μžˆλŠ”μ§€, μ—°κ΄€λœ 개발 νŠΈλ Œλ“œκ°€ 무엇이 μžˆμ„μ§€ μ‚΄νŽ΄λ³΄λ©΄ κ½€ μœ μ΅ν•œ ν•™μŠ΅μ΄ 될 것이닀.

참고 자료

μ•ˆλ„ν˜•2022.03.31
Back to list