๐Ÿ–ฅ๏ธ Native Stack ์ ์šฉํ•˜๊ธฐ

@Troy ยท July 06, 2024 ยท 37 min read

2๋ถ„๊ธฐ TechOKR ์ž‘์—…์œผ๋กœ ์„ ์ •๋œ ํ™”๋ฉด์ „ํ™˜๊ฐ„ ์„ฑ๋Šฅ ๊ฐœ์„  ์ž‘์—…์„ ๋‹ด๋‹นํ•˜๋ฉด์„œ Native Stack Navigator์„ ์ œํ’ˆ์— ๋„์ž…ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ํ•ด๋‹น ์ž‘์—…์„ ์ง„ํ–‰ํ•˜๊ฒŒ๋œ ๋ฐฐ๊ฒฝ, ์ ์šฉ๊ณผ์ •์— ๋Œ€ํ•ด ์ •๋ฆฌํ•˜๋ฉด์„œ ์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ๋œ ๋‚ด์šฉ, ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๊ธฐ๋กํ•ด๋ณด๋ ค ํ•œ๋‹ค.

๐Ÿš€ Native Stack Navigator ์ž‘์—… ๋ฐฐ๊ฒฝ

Native Stack์ด๋ž€

Native Stack Navigator๋Š” react navigation์—์„œ ์ง€์›ํ•˜๋Š” Navigator ํ˜•ํƒœ ์ค‘ ํ•˜๋‚˜๋กœ, stack navigator์˜ ์ธํ„ฐํŽ˜์ด์Šค์™€ ์œ ์‚ฌํ•˜๊ฒŒ ์ œ๊ณตํ•˜๋ฉด์„œ, stack navigator์™€ ๋‹ค๋ฅด๊ฒŒ iOS๋Š” UINavigationController, Android๋Š” Fragment๋กœ Native ์š”์†Œ๋ฅผ ์ด์šฉํ•ด ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋œ๋‹ค.

Native ์š”์†Œ๋ฅผ ์ด์šฉํ•ด ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ Native๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์„ฑ๋Šฅ๊ณผ ํŠน์ง•๋“ค์„ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์„ ๊ฐ€์ง€์ง€๋งŒ, stackNavigator์™€ ๋‹ค๋ฅด๊ฒŒ ์ปค์Šคํ…€์ด ์–ด๋ ค์šด ๋‹จ์ ์„ ๊ฐ€์ง„๋‹ค.

์™œ Native Stack์„ ๋„์ž…ํ•˜๊ฒŒ ๋˜์—ˆ๋‚˜

Native Stack์€ ์ด์ „ 1๋ถ„๊ธฐ์— startup-time ๊ฐœ์„  ์ž‘์—…์„ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๊ณ ๋ คํ–ˆ๋˜ ๋ฐฉ๋ฒ•์ค‘์— ์žˆ์—ˆ๋˜ ์ž‘์—…์œผ๋กœ, ๋‹น์‹œ์—๋Š” ํ™”๋ฉด์ „ํ™˜๊ฐ„ ์†๋„๊ฐ€ ์•ฑ ์‹œ์ž‘ ์‹œ๊ฐ„์„ ์ตœ์ ํ™”ํ•˜๋Š”๋ฐ ํฐ ์˜ํ–ฅ์ด ์—†์„ ๊ฒƒ ๊ฐ™์•„ ๋ณด๋ฅ˜ํ•ด ๋‘์—ˆ๋‹ค.

๋‹น์‹œ ๊ธฐํšํ•ด๋‘์—ˆ๋˜ ์•„์ด๋””์–ด๋“ค
๋‹น์‹œ ๊ธฐํšํ•ด๋‘์—ˆ๋˜ ์•„์ด๋””์–ด๋“ค

์ดํ›„์— ์ผ๊ฐ์œผ๋กœ ๋ฐœ์ „์‹œ์ผฐ๋˜ ์ด์œ ๋กœ ๊ธฐ์กด ์ œํ’ˆ์— stackNavigator๋ฅผ ์ด์šฉํ•˜๋ฉด์„œ ํ™”๋ฉด ์ „ํ™˜๊ฐ„ ๋ฒ„๋ฒ…์ž„์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๊ฐ„ํ—์ ์œผ๋กœ ์žˆ์—ˆ๊ณ , React Native ๊ณต์‹๋ฌธ์„œ์˜ Navigation ์˜ˆ์ œ๊ฐ€ NativeStack์„ ์ด์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์†Œ๊ฐœ๋˜๋Š” ๊ฒƒ์œผ๋กœ ์ˆ˜์ •๋˜์—ˆ๊ณ  (์ปค๋ฎค๋‹ˆํ‹ฐ์—์„œ ์„ฑ๋Šฅ์„ ์œ„ํ•ด ๊ถŒ์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•), ํ™”๋ฉด์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ Native ์Šค๋ ˆ๋“œ์—์„œ ์ง„ํ–‰ํ•˜๊ฒŒ ๋˜๋ฉด JS ์Šค๋ ˆ๋“œ๊ฐ€ ๋ฐ”์˜๊ฒŒ ์ง„ํ–‰๋  ๋•Œ์—๋„ ์•ˆ์ •์ ์œผ๋กœ ํ™”๋ฉด ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์„ ๊ธฐ๋Œ€ํ•˜๋ฉฐ ์ž‘์—…์„ ์‹œ์ž‘ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

[react native ๊ณต์‹๋ฌธ์„œ์˜ Navigating between screens]

react native ๊ณต์‹๋ฌธ์„œ์˜ Navigating between screens
react native ๊ณต์‹๋ฌธ์„œ์˜ Navigating between screens

[React Navigation v2์˜ ์†Œ๊ฐœ๋œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋น„๊ต ๋ฐ์ดํ„ฐ]

React Navigation v2์˜ ์†Œ๊ฐœ๋œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋น„๊ต ๋ฐ์ดํ„ฐ
React Navigation v2์˜ ์†Œ๊ฐœ๋œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋น„๊ต ๋ฐ์ดํ„ฐ

์ถ”๊ฐ€๋กœ ํ˜„์žฌ React Native๋Š” Expo๋ฅผ ๊ณต์‹์ ์ธ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ์ถ”์ฒœํ•˜๊ณ  ์žˆ๋Š” ํ๋ฆ„ ์†์— expo์—์„œ ์‚ฌ์šฉํ•˜๋Š” routing ์‹œ์Šคํ…œ์ธ Expo router๋„ ๋™์ผํ•˜๊ฒŒ React Native Screens๋ฅผ ์ด์šฉํ•ด file-based routing ๋ฐฉ์‹์œผ๋กœ ์ง€์›ํ•˜๊ณ  ์žˆ๋‹ค.

expo-router ์ธํŠธ๋กœ ์„น์…˜
expo-router ์ธํŠธ๋กœ ์„น์…˜

๐Ÿ›  Native Stack Navigator ์ œํ’ˆ์— ์ ์šฉํ•ด๋ณด๊ธฐ

๊ธฐ์กด ์ œํ’ˆ์€ Stack Navigator๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— Native Stack Navigator๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Stack Navigator์˜ Navigation Option์„ Native Stack Navigator์— ๋งž๊ฒŒ ๋ณ€๊ฒฝํ•˜๋Š” ์ž‘์—…์ด ๊ฐ€์žฅ ์ค‘์š”ํ•˜๊ฒŒ ์ง„ํ–‰๋˜์—ˆ๋‹ค.

์ด๋Ÿฌํ•œ ์˜ต์…˜์ค‘ ๊ฐ€์žฅ ์ค‘์š”ํ–ˆ๋˜ ๋ถ€๋ถ„์€ presentation ์˜ต์…˜์œผ๋กœ, presentation์„ ์–ด๋–ป๊ฒŒ ์ •ํ•˜๋Š๋ƒ์— ๋”ฐ๋ผ ํ™”๋ฉด์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜, ํ™”๋ฉด ๋ Œ๋”๋ง ์Šคํƒ€์ผ์ด ๋‹ฌ๋ผ์ง€๊ฒŒ ๋œ๋‹ค.

Stack Navigator์˜ presentation ์˜ต์…˜

stack navigator์—์„œ๋Š” card, modal, transparent modal 3๊ฐ€์ง€ ์˜ต์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • card: ๊ธฐ๋ณธ ํ™”๋ฉด์ „ํ™˜ ๋ฐฉ์‹์œผ๋กœ, iOS์™€ Android์—์„œ default OS animation์œผ๋กœ ํ™”๋ฉด์ „ํ™˜์ด ์ง„ํ–‰๋œ๋‹ค.
  • modal: ํ™”๋ฉด์ด ๋ชจ๋‹ฌ๋กœ ๋œจ๋Š” ๋ฐฉ์‹์œผ๋กœ, iOS์™€ android ๋ชจ๋‘ ํ™”๋ฉด์ด ์•„๋ž˜์—์„œ ์œ„๋กœ ์˜ฌ๋ผ์˜ค๋Š” ๋ฐฉ์‹์œผ๋กœ ํ™”๋ฉด์ด ๋œจ๊ฒŒ ๋œ๋‹ค.
  • transparent modal: ๋ชจ๋‹ฌ๋กœ ๋œจ๋Š” ํ™”๋ฉด์ด์ง€๋งŒ, ๋ฐฐ๊ฒฝ์ด ํˆฌ๋ช…ํ•˜๊ฒŒ ๋˜์–ด์žˆ์–ด ์ด์ „ ํ™”๋ฉด์ด ๋ณด์ด๊ฒŒ ๋œ๋‹ค.

[iOS Presentation๋ณ„ ํ™”๋ฉด์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜]

Card Modal Transparent Modal
js-card-ios js-modal-ios js-transparent-ios.gif

[Android Presentation๋ณ„ ํ™”๋ฉด์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜]

Card Modal Transparent Modal
modal js-modal-android.gif js-transparent-android.gif

๋˜ํ•œ, stack Navigator์˜ ๊ฒฝ์šฐ ์ผ๋ฐ˜ View๋กœ ๊ตฌํ˜„๋˜๋Š” ํ™”๋ฉด์ด๊ธฐ ๋•Œ๋ฌธ์— Card <-> Modal ํ™”๋ฉด๊ฐ„ ์ด๋™์ด ์ž์œ ๋กœ์›Œ Card ์œ„์— Modalํ™”๋ฉด์ด ์Œ“์ด๊ณ , Modalํ™”๋ฉด์— Card ํ™”๋ฉด์ด ๋‹ค์‹œ ์Œ“์ผ ์ˆ˜ ์žˆ๋‹ค.

[Card -> Modal -> Card2 -> TransparentModal ํ™”๋ฉด์ „ํ™˜]

modal

Native Stack Navigator์˜ presentation ์˜ต์…˜

Native Stack Navigator์—์„œ๋Š” card, modal, transparent modal, contained modal, contained transparent modal, full screen modal, form sheet 7๊ฐ€์ง€๋กœ ๊ตฌ๋ถ„๋˜์–ด ์žˆ๋‹ค.

iOS์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋‹ฌ ์Šคํƒ€์ผ์„ ์กฐ๊ธˆ ๋” ์„ธ๋ถ€์ ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ง€์›ํ•˜๊ณ  ์žˆ๊ณ , android์—์„œ๋Š” ๋ชจ๋‘ modal ๋˜๋Š” transparentModal๋กœ fallback๋˜์–ด ์ฒ˜๋ฆฌ๋œ๋‹ค.

  • card: ๊ธฐ๋ณธ ํ™”๋ฉด์ „ํ™˜ ๋ฐฉ์‹์œผ๋กœ, iOS๋Š” ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ์œผ๋กœ ํ™”๋ฉด์ด ์ „ํ™˜๋˜๊ณ , Android๋Š” OS ๋ฒ„์ „์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ํ™”๋ฉด์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ง„ํ–‰๋œ๋‹ค.
  • modal: iOS๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ๋ชจ๋‹ฌ์ฒ˜๋Ÿผ ํ™”๋ฉด์ด ์ž…์ฒด์ ์œผ๋กœ ์˜ฌ๋ผ์˜ค๋Š” ํ˜•ํƒœ์˜ ๋ชจ๋‹ฌ์„ ๊ฐ€์ง€๊ณ  ์•„๋ž˜์—์„œ ์œ„๋กœ ํ™”๋ฉด์ด ๋‚˜ํƒ€๋‚˜๊ฒŒ ๋˜์ง€๋งŒ, Android๋Š” card์™€ ๋™์ผํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ํ™”๋ฉด์ด ์ „ํ™˜๋œ๋‹ค. (๊ด€๋ จ ์ด์Šˆ)
  • transparent modal: ์ด์ „ํ™”๋ฉด์ด ๋ณด์ด๋Š” ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๋ณด์ด๋Š” ๋ชจ๋‹ฌํ˜•ํƒœ์˜ ํ™”๋ฉด์ด๋‹ค.
  • contained modal: iOS๋Š” UIModalPresentationCurrentContext ๋ชจ๋‹ฌ ์Šคํƒ€์ผ์„ ์ด์šฉํ•ด ๋ถ€๋ชจ ํฌ๊ธฐ์— ๋”ฐ๋ผ ์ฐจ์ง€ํ•˜๊ฒŒ ๋˜๋ฉฐ, Android๋Š” modal๊ณผ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋œ๋‹ค.
  • contained transparent modal: iOS๋Š” UIModalPresentationOverCurrentContext ๋ชจ๋‹ฌ ์Šคํƒ€์ผ์„ ์ด์šฉํ•ด ๋ถ€๋ชจ ํฌ๊ธฐ์— ๋”ฐ๋ผ ์ฐจ์ง€ํ•˜๊ฒŒ ๋˜๋ฉฐ, Android๋Š” transparent modal๊ณผ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋œ๋‹ค.
  • fullScreenModal: iOS๋Š” UIModalPresentationFullScreen ๋ชจ๋‹ฌ ์Šคํƒ€์ผ์„ ์ด์šฉํ•ด ์ „์ฒด ํ™”๋ฉด์„ ์ฐจ์ง€ํ•˜๊ฒŒ ๋˜๊ณ , ์ œ์Šค์ฒ˜๋กœ ์ œ๊ฑฐ๋˜์ง€ ์•Š๋Š” ํŠน์ง•์„ ๊ฐ€์ง„๋‹ค. Android๋Š” modal๊ณผ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋œ๋‹ค.
  • formSheet: iOS๋Š” UIModalPresentationFormSheet ๋ชจ๋‹ฌ ์Šคํƒ€์ผ์„ ์ด์šฉํ•˜๊ณ  Android๋Š” modal๊ณผ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋œ๋‹ค.

[iOS Presentation]

card modal
card modal
transparent modal contained modal
card modal
contained transparent modal fullScreen modal
card modal
formSheet
card

[android Presentation (Android 14, API Level 34)]

card modal
card modal
transparent modal contained modal
card modal
contained transparent modal fullScreen modal
card modal
formSheet
card

stack Navigator์™€ ๋‹ค๋ฅด๊ฒŒ Native ์š”์†Œ๋“ค์„ ์ด์šฉํ•ด ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋‹ฌ์€ ํ•ญ์ƒ Navigation History์˜ ๋งˆ์ง€๋ง‰์— ์™€์•ผํ•˜๋Š” ์กฐ๊ฑด์ด ์žˆ๋‹ค. ์ด ์กฐ๊ฑด์„ ์ง€ํ‚ค์ง€ ์•Š์œผ๋ฉด Card ํ™”๋ฉด์ด Modal ํ™”๋ฉด ๋’ค์— ์Œ“์—ฌ ๋ณด์ด์ง€ ์•Š๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•œ๋‹ค.

[Card -> Modal -> Card2 ํ™”๋ฉด์ „ํ™˜ ]

modal

์œ„ ์˜ˆ์‹œ๋ฅผ ๋ณด๋ฉด Modal ์ดํ›„ Card2๋กœ ํ™”๋ฉด์ „ํ™˜์„ ์ง„ํ–‰ํ•˜๊ฒŒ ๋˜๋ฉด, Card2๊ฐ€ Modal ํ™”๋ฉด ๋’ค์— ์Œ“์ด๊ฒŒ ๋˜์–ด ๋ณด์ด์ง€ ์•Š๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•œ๋‹ค. navigation pop์„ ํด๋ฆญํ•˜๊ฒŒ ๋˜๋ฉด, Card2๊ฐ€ ๋จผ์ € ์ œ๊ฑฐ๋˜๊ณ , ์ดํ›„ Modal ํ™”๋ฉด์ด ์ œ๊ฑฐ๋˜๋Š” ํ˜„์ƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

React Navigation์˜ ๊ฐ€์ด๋“œ์— ๋”ฐ๋ฅด๋ฉด ๋ชจ๋‹ฌ ํ™”๋ฉด์€ ํ•ญ์ƒ Navigation History์˜ ๋งˆ์ง€๋ง‰์— ์™€์•ผํ•œ๋‹ค๊ณ  ๋˜์–ด์žˆ๋Š”๋ฐ, ์ด๋Š” Native Stack Navigator์˜ ํŠน์ง•์œผ๋กœ ๋ณด์—ฌ์ง„๋‹ค.

React Navigation์˜ ๊ฐ€์ด๋“œ
React Navigation์˜ ๊ฐ€์ด๋“œ

์ œํ’ˆ ๋‚ด Navigator presentation ๋ฐ˜์˜ํ•˜๊ธฐ

๊ทธ๋Ÿฌ๋ฉด ์ด์ œ Native Stack Navigator์˜ presentation ์˜ต์…˜์„ ์ œํ’ˆ์— ์ ์šฉํ•ด๋ณด์ž.

Card

Stack Navigator์™€ ๋™์ผํ•˜๊ฒŒ iOS์™€ Android ๋ชจ๋‘ ๋™์ผํ•˜๊ฒŒ Card์— default Animation์„ ์ ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. iOS๋Š” ์™„์ „ํžˆ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•ด์„œ ํฐ ๊ณ ๋ฏผ์ด ์—†์—ˆ์ง€๋งŒ Android๋Š” ๋ณ„๋„์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ฃผ์–ด์•ผํ•  ์ง€ ๊ณ ๋ฏผ์ด ๋˜์—ˆ๋‹ค. ๊ทธ์ด์œ ๋Š” Android์—์„œ๋Š” OS ๋ฒ„์ „์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ํ™”๋ฉด์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ง„ํ–‰๋˜๊ฒŒ ๋˜๊ณ  ๊ธฐ์กด๊ณผ ๋‹ค๋ฅธ ์œ ์ € ๊ฒฝํ—˜์— ๋Œ€ํ•œ ์šฐ๋ ค๊ฐ€ ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

[Stack Navigator์— ์ •์˜๋œ Transition Preset, Android ๋ฒ„์ „๋ณ„ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์˜ต์…˜]

/**
 * Standard Android navigation transition when opening or closing an Activity on Android < 9 (Oreo).
 */
export const FadeFromBottomAndroid: TransitionPreset = {
  gestureDirection: 'vertical',
  transitionSpec: {
    open: FadeInFromBottomAndroidSpec,
    close: FadeOutToBottomAndroidSpec,
  },
  cardStyleInterpolator: forFadeFromBottomAndroid,
  headerStyleInterpolator: forFade,
};

/**
 * Standard Android navigation transition when opening or closing an Activity on Android 9 (Pie).
 */
export const RevealFromBottomAndroid: TransitionPreset = {
  gestureDirection: 'vertical',
  transitionSpec: {
    open: RevealFromBottomAndroidSpec,
    close: RevealFromBottomAndroidSpec,
  },
  cardStyleInterpolator: forRevealFromBottomAndroid,
  headerStyleInterpolator: forFade,
};

/**
 * Standard Android navigation transition when opening or closing an Activity on Android 10 (Q).
 */
export const ScaleFromCenterAndroid: TransitionPreset = {
  gestureDirection: 'horizontal',
  transitionSpec: {
    open: ScaleFromCenterAndroidSpec,
    close: ScaleFromCenterAndroidSpec,
  },
  cardStyleInterpolator: forScaleFromCenterAndroid,
  headerStyleInterpolator: forFade,
};

์•„๋ž˜ GIF์—์„œ ๊ธฐ์กด์€ ๊ฐ€์šด๋ฐ์—์„œ ํผ์ ธ๋‚˜๊ฐ€๋Š” ํ˜•์‹(ScaleFromCenterAndroid)์œผ๋กœ ํ™”๋ฉด์ด ์ „ํ™˜๋œ๋‹ค๋ฉด, Native Stack์—์„œ๋Š” ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ์œผ๋กœ ํ™”๋ฉด์ด ์ „ํ™˜๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ธฐ์กด ์•ˆ๋“œ๋กœ์ด๋“œ ํ™”๋ฉด์ „ํ™˜ Native Stack ํ™”๋ฉด ์ „ํ™˜
card modal

๊ธฐ์กด ScaleFromCenterAndroid์™€ ์ตœ๋Œ€ํ•œ ์œ ์‚ฌํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ Fade ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ฒ˜์Œ ๊ณ ๋ฏผํ•ด๋ดค์ง€๋งŒ, ์—ฌ์ „ํžˆ ๊ธฐ์กด๊ณผ ๋‹ค๋ฅธ๊ฒŒ ๋Š๊ปด์ง€๋Š” ๊ฒƒ ๊ฐ™๋‹ค๋Š” ๋™๋ฃŒ๋ถ„์˜ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์•˜๊ณ , ๋ฒ„์ „์— ๋งž๊ฒŒ ํ‘œ์ค€ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•˜๋Š” default ์˜ต์…˜์ด ์ดํ›„ ์œ ์ง€๋ณด์ˆ˜ ์ธก๋ฉด์—์„œ ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ข‹์€ ์กฐ์–ธ์„ ํ•ด์ฃผ์…”์„œ, Card ์˜ต์…˜์„ ๊ทธ๋Œ€๋กœ ์ ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

Modal

์ž‘์—… ์ค‘ ๊ฐ€์žฅ ์ด์Šˆ๊ฐ€ ๋งŽ์•˜๊ณ , OS๋ณ„๋กœ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ๋ถ€๋ถ„์ด ๋‹ฌ๋ผ ๊ณ ๋ฏผ์ด ๋งŽ์•˜๋˜ ์ž‘์—… ์˜์—ญ์ด์—ˆ๋‹ค.

OS๋ณ„๋กœ ๋ชจ๋‹ฌ ์˜ต์…˜๋“ค์„ ์ •๋ฆฌํ•ด๋ณด๋ฉด iOS์—์„œ๋Š” modal, contained modal, fullscreen modal, formsheet 4๊ฐ€์ง€๊ฐ€ ์žˆ๊ณ , android๋Š” modal ํ•œ๊ฐ€์ง€ ์˜ต์…˜๋งŒ ์ œ๊ณตํ•˜์ง€๋งŒ ์ด์Šˆ๊ฐ€ ์žˆ์–ด์„œ ์ถ”๊ฐ€์ ์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์˜ต์…˜์„ ์ ์šฉํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์ด์—ˆ๋‹ค.

์—ฌ๊ธฐ์— ์ถ”๊ฐ€์ ์œผ๋กœ ๋ชจ๋‹ฌ์ด ํ•ญ์ƒ Navigation History์˜ ๋งˆ์ง€๋ง‰์— ์™€์•ผํ•˜๋Š” ์กฐ๊ฑด์ด ์žˆ์–ด์„œ, ์ด๋ฅผ ์ง€ํ‚ค์ง€ ์•Š์œผ๋ฉด ๊ธฐ์กด ์ œํ’ˆ๊ณผ ๋‹ค๋ฅด๊ฒŒ ํ™”๋ฉด์ด ๋ณด์ด์ง€ ์•Š๋Š” ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•ด, card์™€ modal๊ฐ„ ํ™”๋ฉด ์ „ํ™˜์˜ ์ž์œ ๋„๋„ ๊ณ ๋ฏผํ•ด์•ผ ํ–ˆ๋‹ค.

๊ฒฐ๋ก ์ ์œผ๋กœ๋Š” ์œ„์—์„œ ์–ธ๊ธ‰ํ•œ ์˜ต์…˜๋“ค์ด ์•„๋‹Œ iOS์™€ Android ๋ชจ๋‘ Card ์˜ต์…˜์— slide_from_bottom ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•œ ํ˜•ํƒœ๋กœ ์ ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

์™œ ๋œฌ๊ธˆ์—†์ด Card๋ƒ๋Š” ์˜๋ฌธ์ด ๋“ค ์ˆ˜ ์žˆ๊ฒ ์ง€๋งŒ, android์— ์ด์Šˆ๋กœ ์ธํ•ด ์ถ”๊ฐ€์ ์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ํ•„์ˆ˜์ ์œผ๋กœ ํ•„์š”ํ•œ ์ƒํ™ฉ์ธ ์ ๊ณผ ๊ธฐ์กด ์ œํ’ˆ ๋‚ด Navigation History ๊ด€๋ฆฌ ๋ฐฉ์‹์„ ์œ ์ง€ํ•ด ์ž‘์—…๋ฒ”์œ„์™€ ์ปจ๋ฒค์…˜์„ ์ง€ํ‚ค๋Š” ๊ฒƒ์ด ๋” ์ข‹๊ฒ ๋‹ค๋Š” ์ ์„ ๊ณ ๋ คํ•ด ์„ ํƒํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ๊ฐ ์˜ต์…˜๋“ค์„ ์ ์šฉํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ–ˆ๋˜ ์ด์Šˆ๋“ค์„ ์ •๋ฆฌํ•ด๋ณด์ž.

Modal์™€ FormSheet

iOS์—์„œ๋Š” modal์™€ formsheet ์˜ต์…˜์„ ์ ์šฉํ–ˆ์„ ๋•Œ, ๊ธฐ์กด ์ œํ’ˆ๋‚ด ๋ชจ๋‹ฌ๊ณผ ๋‹ค๋ฅด๊ฒŒ ํ™”๋ฉด ์ „์ฒด๋ฅผ ์ฐจ์ง€ํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ์ผ์ • ์˜์—ญ๋งŒ ์ฐจ์ง€ํ•˜๊ณ  ์œ„๋กœ ๋– ์žˆ๋Š” ํ˜•ํƒœ๋ฅผ ๊ฐ€์ง€๊ฒŒ ๋˜๊ณ , android๋Š” card์™€ ๋™์ผํ•˜๊ฒŒ ํ™”๋ฉด ์ „ํ™˜์ด ์ด๋ฃจ์–ด์ง€๋Š” ์ด์Šˆ๊ฐ€ ์žˆ์–ด ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ–ˆ๋‹ค.

[์ œํ’ˆ๋‚ด ๋ชจ๋‹ฌ๊ณผ Modal ์˜ต์…˜]

๊ธฐ์กด ์ œํ’ˆ๋‚ด ๋ชจ๋‹ฌ ํ™”๋ฉด modal iOS modal android
card card modal

[์ œํ’ˆ๋‚ด ๋ชจ๋‹ฌ๊ณผ formsheet ์˜ต์…˜]

๊ธฐ์กด ์ œํ’ˆ๋‚ด ๋ชจ๋‹ฌ ํ™”๋ฉด formsheet iOS formsheet android
card modal modal
Contained Modal๊ณผ FullScreen Modal

android๋Š” card์™€ ๋™์ผํ•˜๊ฒŒ ํ™”๋ฉด ์ „ํ™˜์ด ์ด๋ฃจ์–ด์ง€๋Š” ์ด์Šˆ๊ฐ€ ๋™์ผํ•˜๊ฒŒ ์žˆ์ง€๋งŒ, iOS์—์„œ๋Š” contained modal๊ณผ fullscreen modal ์˜ต์…˜์„ ์ ์šฉํ–ˆ์„ ๋•Œ, ๊ธฐ์กด๊ณผ ๊ฐ™์ด ํ™”๋ฉด์ด ์ „์ฒด๋ฅผ ์ฐจ์ง€ํ•˜๋Š” ํ˜•ํƒœ๋กœ ํ™”๋ฉด์ด ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ์–ด ์‚ฌ์šฉํ•˜๋ ค ํ–ˆ๋˜ ์˜ต์…˜์ด์—ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ฐ ์˜ต์…˜์€ ์ ์šฉ์— ์žˆ์–ด ๋ฌธ์ œ์ ๋“ค์ด ๊ฐ๊ฐ ์กด์žฌํ–ˆ๋‹ค. contained Modal์˜ ๊ฒฝ์šฐ์—๋Š” Navigation History์˜ ๋งˆ์ง€๋ง‰์— ์™€์•ผํ•˜๋Š” ์กฐ๊ฑด์„ ์œ„ํ•ด ๋ณ„๋„์˜ Nested Navigator๋กœ ๋ชจ๋‹ฌ๋“ค์„ ๊ด€๋ฆฌํ•˜๋Š” ๋น„์šฉ์ด ์ปธ๋‹ค๋Š” ์ ์œผ๋กœ ์ธํ•ด ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ–ˆ๋‹ค.

fullscreen Modal ์˜ต์…˜์€ card,modal๊ฐ„ ํ™”๋ฉด ์ „ํ™˜ ์ด์Šˆ์™€ ๋”๋ถˆ์–ด, alert ๋ชจ๋‹ฌ์„ ์ƒ์œ„์— ๋„์›Œ์ค„ ์ˆ˜ ์—†๋Š” ์ด์Šˆ๊ฐ€ ์žˆ์–ด ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ–ˆ๋‹ค. ์•„๋ž˜๋Š” ํ•ด๋‹น ์ƒํ™ฉ์„ ์œ„ํ•œ mimic ์ฝ”๋“œ์ด๋‹ค.

[fullscreen modal์—์„œ Modal๋กœ ๊ตฌํ˜„ํ•œ alert๊ฐ€ ๋œจ์ง€ ์•Š๋Š” ์ด์Šˆ๋ฅผ ์œ„ํ•œ mimic ์ฝ”๋“œ]

// zustand๋กœ ๊ตฌํ˜„ํ•œ ์ „์—ญ ๋ชจ๋‹ฌ ๋…ธ์ถœ ์ฝ”๋“œ
import {create} from 'zustand';

export const useAlertModal = create(set => ({
    visible: false,
    show: () => set({visible: true}),
    hide: () => set({visible: false}),
}));



const App = () => {
  const visible = useAlertModal(state => state.visible);
  const close = useAlertModal(state => state.hide);
  const onPressClose = () => {
    close();
  };

  return (
    <SafeAreaView style={{flex: 1}}>
      {visible && (
        <Modal
          visible={visible}
          animationType={'fade'}
          transparent={true}
          onRequestClose={onPressClose}>
          <View
            style={{
              justifyContent: 'center',
              alignItems: 'center',
              backgroundColor: 'white',
              flex: 1,
            }}>
            <Text>Modal์ด์—์š”</Text>
            <Button title="๋‹ซ๊ธฐ" onPress={onPressClose} />
          </View>
        </Modal>
      )}
      <NavigationContainer>
        <NativeStack />
      </NavigationContainer>
    </SafeAreaView>
  );
};

const FullscreenModal = ({navigation}) => {
    const handlePress = () => {
        navigation.pop();
    };

    const show = useAlertModal(state => state.show);
    const showAlert = () => {
        show();
    };

    return (
        <SafeAreaView style={{flex: 1}}>
            <Button title="๋’ค๋กœ๊ฐ€๊ธฐ" onPress={handlePress} />
            <Button title="alert ๋„์šฐ๊ธฐ" onPress={showAlert} />
        </SafeAreaView>
    );
};
Modal๋กœ ๊ตฌํ˜„ํ•œ alert ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ƒ์œ„์— ๋œจ์ง€ ์•Š๋Š” ์ด์Šˆ
modal

์ด๋Ÿฌํ•œ ์ด์Šˆ๋“ค๋กœ ์ธํ•ด iOS์™€ Android ๋ชจ๋‘ Card ์˜ต์…˜์— slide_from_bottom ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•œ ํ˜•ํƒœ๋กœ ์ ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

slidefrombottom ์†๋„ ์ด์Šˆ ํ•ด๊ฒฐํ•˜๊ธฐ

์ด์ œ ๋”์ด์ƒ ์ด์Šˆ๊ฐ€ ์—†์„ ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ–ˆ์ง€๋งŒ Card ์˜ต์…˜์— slide_from_bottom ์˜ต์…˜์„ ์ ์šฉํ•˜๋Š” ๋ฐฉ์‹์—๋„ ์ด์Šˆ๊ฐ€ ์žˆ์—ˆ๋‹ค.

๋ฐ”๋กœ ๋ชจ๋‹ฌ์ด ๋œจ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ์†๋„ ์ด์Šˆ๋กœ iOS์—์„œ๋Š” duration์„ ์˜ต์…˜์œผ๋กœ ์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ œ๊ณตํ•˜์ง€๋งŒ, android์—์„œ๋Š” ์ปค์Šคํ…€ํ•  ์ˆ˜ ์—†์–ด ๊ธฐ์กด๊ณผ ์ฒด๊ฐ์ด ๋ ์ •๋„๋กœ ๋Š๋ฆฌ๊ฒŒ ํ™”๋ฉด์ด ์ „ํ™˜๋˜๋Š” ์ด์Šˆ๊ฐ€ ์žˆ์—ˆ๋‹ค.

animationDuration ๊ณต์‹๋ฌธ์„œ
animationDuration ๊ณต์‹๋ฌธ์„œ

Android Stack Navigator Modal Android Native Stack Card (slide_from_bottom)
modal modal

๋ˆˆ์— ๋„๊ฒŒ ๋Š๋ฆฌ๋‹ค๋Š” ๋Š๋‚Œ์ด ๋“ค์–ด ํ•ด๋‹น ๋ถ€๋ถ„์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด React Navigation ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ถ„์„ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

๋จผ์ € NativeStackNavigator๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ createNativeStackNavigator๋ฅผ ๋ณด๋ฉด createNavigatorFactory ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜์— NativeStackNavigator๋ฅผ ์ธ์ž๋กœ ๋„˜๊ฒจ์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ NativeStackNavigator๋ฅผ ๋งŒ๋“ค๊ณ  ์žˆ๋‹ค.

//[์ฐธ์กฐ ์ฝ”๋“œ](https://github.com/react-navigation/react-navigation/blob/main/packages/native-stack/src/navigators/createNativeStackNavigator.tsx)
function NativeStackNavigator({...rest }: NativeStackNavigatorProps) {
    ...

    return (
        <NavigationContent>
            <NativeStackView
                {...rest}
                state={state}
                navigation={navigation}
                descriptors={descriptors}
            />
        </NavigationContent>
    );
}

export function createNativeStackNavigator<
    ParamList extends ParamListBase,
    NavigatorID extends string | undefined = undefined,
    TypeBag extends NavigatorTypeBagBase = {
        ParamList: ParamList;
        NavigatorID: NavigatorID;
        State: StackNavigationState<ParamList>;
        ScreenOptions: NativeStackNavigationOptions;
        EventMap: NativeStackNavigationEventMap;
        NavigationList: {
            [RouteName in keyof ParamList]: NativeStackNavigationProp<
                ParamList,
                RouteName,
                NavigatorID
            >;
        };
        Navigator: typeof NativeStackNavigator;
    },
    Config extends StaticConfig<TypeBag> | undefined =
            | StaticConfig<TypeBag>
        | undefined,
>(config?: Config): TypedNavigator<TypeBag, Config> {
    return createNavigatorFactory(NativeStackNavigator)(config);
}

๋‚ด๊ฐ€ ๊ถ๊ธˆํ•œ๊ฑด ์Šคํฌ๋ฆฐ์ด ์–ด๋–ป๊ฒŒ ๋งŒ๋“ค์–ด์ง€๋ƒ๋‹ˆ๊นŒ ์ด์ œ NativeStackView ์ฝ”๋“œ ๋‚ด๋ถ€๋ฅผ ๋ณด๊ฒŒ ๋˜๋ฉด React Native Screens์—์„œ Screen, ScreenStack ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฐ›์•„์„œ prop์„ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ wrappingํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

//[์ฐธ์กฐ ์ฝ”๋“œ](https://github.com/react-navigation/react-navigation/blob/main/packages/native-stack/src/views/NativeStackView.native.tsx):`react-navigation/packages/native-stack/src/views/NativeStackView.native.tsx`)
//...
import {
    Screen,
    type ScreenProps,
    ScreenStack,
    type StackPresentationTypes,
} from 'react-native-screens';
//...


const MaybeNestedStack = (props) => {
    //...
    if (isHeaderInModal) {
        return (
            <ScreenStack style={styles.container}>
                <Screen
                    enabled
                    isNativeStack
                    hasLargeHeader={options.headerLargeTitle ?? false}
                    style={StyleSheet.absoluteFill}
                >
                    {content}
                    <HeaderConfig
                        {...options}
                        route={route}
                        headerHeight={headerHeight}
                        headerTopInsetEnabled={headerTopInsetEnabled}
                        canGoBack
                    />
                </Screen>
            </ScreenStack>
        );
    }

    return content;
};

const SceneView = (props) => {
    //...
    return (
        <Screen
            key={route.key}
            enabled
            isNativeStack
            style={StyleSheet.absoluteFill}
            hasLargeHeader={options.headerLargeTitle ?? false}
            customAnimationOnSwipe={animationMatchesGesture}
            fullScreenSwipeEnabled={fullScreenGestureEnabled}
            // ... ๊ธฐํƒ€ props
        >{...}</Screen>
    );
};

type Props = {
    state: StackNavigationState<ParamListBase>;
    navigation: NativeStackNavigationHelpers;
    descriptors: NativeStackDescriptorMap;
};

export function NativeStackView({ state, navigation, descriptors }: Props) {
    ...
    return (
        <SafeAreaProviderCompat style={{ backgroundColor: colors.background }}>
            <ScreenStack style={styles.container}>
                {state.routes.map((route, index) => {
                    //...
                    return (
                        <SceneView
                            key={route.key}
                            index={index}
                            focused={isFocused}
                            descriptor={descriptor}
                            previousDescriptor={previousDescriptor}
                            nextDescriptor={nextDescriptor}
                            isPresentationModal={isModal}
                            // ๊ธฐํƒ€ props
                        />
                    );
                })}
            </ScreenStack>
        </SafeAreaProviderCompat>
    );
}

์‹ค์ œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์†๋„๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” React Navigation ์ฝ”๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ React Native Screens ํŒจํ‚ค์ง€ ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด์•ผํ•˜๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ React Navigation์—์„œ Native Stack์˜ ์ด์Šˆ๋Š” React Native Screens ๋ ˆํฌ์— ์˜ฌ๋ ค๋‹ฌ๋ผํ–ˆ๊ตฌ๋‚˜ ์ดํ•ด๊ฐ€ ๋˜์—ˆ๋‹ค.

React Native Screens๋กœ ์•ˆ๋‚ดํ•˜๋Š” React Navigation ๊ณต์‹๋ฌธ์„œ
React Native Screens๋กœ ์•ˆ๋‚ดํ•˜๋Š” React Navigation ๊ณต์‹๋ฌธ์„œ

์ด์–ด์„œ React Native Screens ํŒจํ‚ค์ง€ ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ์•„๋ž˜์™€ ๊ฐ™์ด Screen ์ฝ”๋“œ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ๊ณ , Screen ์ปดํฌ๋„ŒํŠธ๋Š” ScreenNativeComponent๋ฅผ ๋งŒ๋“ค์–ด์ง€๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

//[์ฐธ์กฐ ์ฝ”๋“œ](react-native-screens/src/components/Screen.tsx)
// ...
import ScreenNativeComponent from '../fabric/ScreenNativeComponent';
import ModalScreenNativeComponent from '../fabric/ModalScreenNativeComponent';

export const NativeScreen: React.ComponentType<ScreenProps> =
    ScreenNativeComponent as React.ComponentType<ScreenProps>;
const AnimatedNativeScreen = Animated.createAnimatedComponent(NativeScreen);

// ...

const Screen: React.FC<ScreenProps> = props => {
    // ...
    return <AnimatedNativeScreen {...props} />;
};

export default Screen;

๊ทธ๋ฆฌ๊ณ  ScreenNativeComponent ์ฝ”๋“œ๋ฅผ ๋ณด๊ฒŒ ๋˜๋ฉด, Screen ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋Š” ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ๋Š” codegen์„ ์ด์šฉํ•ด Screen ์ปดํฌ๋„ŒํŠธ๋ฅผ iOS์™€ Android๋ฅผ ๋นŒ๋“œํ•  ๋•Œ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋“ค์ด ์ •์˜๋˜์–ด ์žˆ๋Š” ์ฝ”๋“œ์ด๋‹ค. ์ด์ œ codegen์„ ํ†ตํ•ด ๋งŒ๋“ค์–ด์ง„ ํ•ด๋‹น ์ฝ”๋“œ ๋ถ€๋ถ„์„ ์ฐพ์•„๊ฐ€๋ฉด ๋“œ๋””์–ด ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ์•ˆ๋“œ๋กœ์ด๋“œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์†๋„๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

//[์ฐธ์กฐ์ฝ”๋“œ](react-native-screens/src/fabric/ScreenNativeComponent.ts)
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
// ...

type StackPresentation =
  | 'push'
  | 'modal'
  | 'transparentModal'
  | 'fullScreenModal'
  | 'formSheet'
  | 'containedModal'
  | 'containedTransparentModal';

type StackAnimation =
  | 'default'
  | 'flip'
  | 'simple_push'
  | 'none'
  | 'fade'
  | 'slide_from_right'
  | 'slide_from_left'
  | 'slide_from_bottom'
  | 'fade_from_bottom'
  | 'ios';

// ...
export default codegenNativeComponent<NativeProps>('RNSScreen', {
  interfaceOnly: true,
});

๋“œ๋””์–ด rnscreens ๋‚ด๋ถ€์˜ ScreenStack ์ฝ”๋“œ์—์„œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์„ค์ • ์ฝ”๋“œ๋ฅผ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๊ทธ์ค‘ ๋‚ด๊ฐ€ ์ฐพ๋˜ slide_from_bottom์• ๋‹ˆ๋ฉ”์ด์…˜์€ R.anim.rns_slide_in_from_bottom, R.anim.rns_no_animation_medium์œผ๋กœ ์ •์˜๋˜์–ด ์žˆ์—ˆ๋‹ค.

[์ฐธ๊ณ  ์ฝ”๋“œ](react-native-screens/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt)
class ScreenStack(context: Context?) : ScreenContainer(context) {
    ...
    override fun onUpdate() {
        ...
        createTransaction().let {
            // animation logic start
            if (stackAnimation != null) {
                if (shouldUseOpenAnimation) {
                    when (stackAnimation) {
                        StackAnimation.DEFAULT -> it.setCustomAnimations(R.anim.rns_default_enter_in, R.anim.rns_default_enter_out)
                        StackAnimation.NONE -> it.setCustomAnimations(R.anim.rns_no_animation_20, R.anim.rns_no_animation_20)
                        StackAnimation.FADE -> it.setCustomAnimations(R.anim.rns_fade_in, R.anim.rns_fade_out)
                        StackAnimation.SLIDE_FROM_RIGHT -> it.setCustomAnimations(R.anim.rns_slide_in_from_right, R.anim.rns_slide_out_to_left)
                        StackAnimation.SLIDE_FROM_LEFT -> it.setCustomAnimations(R.anim.rns_slide_in_from_left, R.anim.rns_slide_out_to_right)
                        StackAnimation.SLIDE_FROM_BOTTOM -> it.setCustomAnimations(
                            R.anim.rns_slide_in_from_bottom, R.anim.rns_no_animation_medium
                        )
                        StackAnimation.FADE_FROM_BOTTOM -> it.setCustomAnimations(R.anim.rns_fade_from_bottom, R.anim.rns_no_animation_350)
                        StackAnimation.IOS -> it.setCustomAnimations(R.anim.rns_slide_in_from_right_ios, R.anim.rns_slide_out_to_left_ios)
                    }
                } else {
                    when (stackAnimation) {
                        StackAnimation.DEFAULT -> it.setCustomAnimations(R.anim.rns_default_exit_in, R.anim.rns_default_exit_out)
                        StackAnimation.NONE -> it.setCustomAnimations(R.anim.rns_no_animation_20, R.anim.rns_no_animation_20)
                        StackAnimation.FADE -> it.setCustomAnimations(R.anim.rns_fade_in, R.anim.rns_fade_out)
                        StackAnimation.SLIDE_FROM_RIGHT -> it.setCustomAnimations(R.anim.rns_slide_in_from_left, R.anim.rns_slide_out_to_right)
                        StackAnimation.SLIDE_FROM_LEFT -> it.setCustomAnimations(R.anim.rns_slide_in_from_right, R.anim.rns_slide_out_to_left)
                        StackAnimation.SLIDE_FROM_BOTTOM -> it.setCustomAnimations(
                            R.anim.rns_no_animation_medium, R.anim.rns_slide_out_to_bottom
                        )
                        StackAnimation.FADE_FROM_BOTTOM -> it.setCustomAnimations(R.anim.rns_no_animation_250, R.anim.rns_fade_to_bottom)
                        StackAnimation.IOS -> it.setCustomAnimations(R.anim.rns_slide_in_from_left_ios, R.anim.rns_slide_out_to_right_ios)
                    }
                }
            }

            ...
        }
    }
}

์ด์ œ ์ง„์งœ ๋งˆ์ง€๋ง‰์œผ๋กœ ํ•ด๋‹น xml ํŒŒ์ผ์„ ์ฐพ์•„ duration๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•˜๊ณ  patch package๋ฅผ ์ง„ํ–‰ํ•ด ์• ๋‹ˆ๋ฉ”์ด์…˜ ์†๋„๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๊ธฐ์กด duration์€ config_mediumAnimTime์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์—ˆ๊ณ , ํ•ด๋‹น ๋ถ€๋ถ„์„ ์ˆ˜์ •ํ•˜๋ฉด ์ปค์Šคํ…€ํ•˜๊ฒŒ ์ˆ˜์ •์ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

animation duration์€ react-native-screens์—์„œ 20,250,350์„ ๊ธฐ๋ณธ ๊ฐ’์œผ๋กœ ์„ค์ •ํ•ด๋‘๊ณ  ์žˆ๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์—ฌ, 250์œผ๋กœ ๋ณ€๊ฒฝํ•ด ์ ์šฉํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

<!--[์ฐธ๊ณ  ์ฝ”๋“œ](react-native-screens/android/src/main/res/base/anim/rns_slide_in_from_bottom.xml)-->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromYDelta="100%"
    android:toYDelta="0%"
    android:duration="@android:integer/config_mediumAnimTime" /> <!--250์œผ๋กœ ๋ณ€๊ฒฝ -->

<!--[์ฐธ๊ณ  ์ฝ”๋“œ](react-native-screens/android/src/main/res/base/anim/rns_slide_out_to_bottom.xml)-->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromYDelta="100%"
    android:toYDelta="0%"
    android:duration="@android:integer/config_mediumAnimTime" /> <!--250์œผ๋กœ ๋ณ€๊ฒฝ -->
    
<!--[์ฐธ๊ณ  ์ฝ”๋“œ] (react-native-screens/android/src/main/res/base/anim/rns_no_animation_medium.xml)-->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="1.0"
    android:toAlpha="1.0"
    android:duration="@android:integer/config_mediumAnimTime"/> <!--250์œผ๋กœ ๋ณ€๊ฒฝ -->  
๊ฐœ์„  ์ „ Android Native Stack Card (slide_from_bottom) ๊ฐœ์„  ํ›„ Android Native Stack Card (slide_from_bottom)
modal modal

์‹œ๊ฐ„์ƒ ํ•˜์ง€ ๋ชปํ–ˆ์ง€๋งŒ, ์กฐ๊ธˆ ๋” ๋ชจ๋‹ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ฐœ์„ ํ•˜๋ฉด ์ข‹์•˜์„ ๊ฒƒ ๊ฐ™๋‹ค๋Š” ์•„์‰ฌ์šด ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. ๊ธฐ์กด์—๋Š” ๋์— ์ ์  ์ฒœ์ฒœํžˆ ๋„์ฐฉํ•˜๋Š” ๋“ฏํ•œ ํšจ๊ณผ๊ฐ€ ์žˆ์—ˆ์ง€๋งŒ ์ง€๊ธˆ์€ linearํ•˜๊ฒŒ ํ•œ๋ฒˆ์— ๋œจ๊ณ  ๋‹ซํžˆ๋Š” ๊ฒƒ ๊ฐ™์•„ ์–ด์ƒ‰ํ•จ์ด ๋‚จ์•„์žˆ์–ด๋ณด์˜€๋‹ค. ์ดํ›„์— ๋” native์Šค๋Ÿฌ์šด ๋Š๋‚Œ์„ ์ค„ ์ˆ˜ ์žˆ๊ฒŒ ํ›„์ž‘์—…์œผ๋กœ ๊ฐœ์„  ์ž‘์—…๋„ ์ง„ํ–‰ํ•ด๋ณด๋ ค ํ•œ๋‹ค.

TransparentModal

๊ธฐ์กด ์ผ๋ถ€ ํ™”๋ฉด์—์„œ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ๋˜ transparent modal ์˜ต์…˜์„ containedTransparentModal์„ ์ด์šฉํ•ด ์ตœ๋Œ€ํ•œ ์‚ฌ์šฉํ•˜๋ ค ํ–ˆ์ง€๋งŒ, ๋‹ค์Œ ํ™”๋ฉด์ด card์ธ ๊ฒฝ์šฐ์— ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋ฒ„๋ฒ…์ด๋Š” ๋“ฏํ•œ ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ transparent modal ์˜ต์…˜ ๋Œ€์‹  ๊ธฐ์กด ํ™”๋ฉด์„ ๋ฐ”ํ…€์‹œํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ด์šฉํ•ด ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ˆ˜์ •ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ–ˆ์„ ๋•Œ ๊ธฐ์กด์€ ๋ชจ๋‹ฌ์ด์ง€๋งŒ ์„œ์„œํžˆ fade in/fade out์œผ๋กœ ํ™”๋ฉด์ „ํ™˜์ด ๋˜์—ˆ์ง€๋งŒ, ๋ฐ”ํ…€์‹œํŠธ๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด์„œ overlay๊ฐ€ ๋ณด์ด๋Š” ๋ชจ๋‹ฌ ๋Š๋‚Œ์ด ๋” ๋“ค ์ˆ˜ ์žˆ์–ด ์ข‹์€ ์„ ํƒ์ด๋ผ ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

์ œํ’ˆ ๋‚ด ๋ชจ๋“  transparent modal ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๋˜ ํ™”๋ฉด๋“ค์„ ๋‹ค ๋ฐ”๊พธ์ง€๋Š” ๋ชปํ–ˆ๊ณ , ์ด์Šˆ๊ฐ€ ์žˆ์—ˆ๋˜ ํ™”๋ฉด๋งŒ ์ˆ˜์ •ํ•˜๊ฒŒ ๋˜์—ˆ์ง€๋งŒ, ๋‹ค์Œํ™”๋ฉด์— ๋”ฐ๋ผ ํ™”๋ฉด์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์— ์ด์Šˆ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์–ด ํ›„์ž‘์—…์„ ์ง„ํ–‰ํ•ด์•ผํ•  ๊ฒƒ ๊ฐ™๋‹ค.

์ด์Šˆ๊ฐ€ ๋˜์—ˆ๋˜ transparent modal BottomSheet์œผ๋กœ ์ „ํ™˜ํ•œ transparent modal
modal modal

๊ธฐํƒ€ ์ด์Šˆ: gesture handler๋กœ ์‚ฌ์ง„ ๋Œ์–ด๋‹น๊ฒจ ๋‹ซ๊ธฐ ์ด์Šˆ

ํ•ด๋‹น ์ด์Šˆ๋Š” ์ฒซ๋ฒˆ์งธ ๋ฐฐํฌ ๋•Œ ๋กค๋ฐฑํ•˜๊ฒŒ ๋œ ๊ฐ€์žฅ ์ปธ๋˜ ์ด์Šˆ๋กœ ์ด๋ฏธ์ง€ ์Šฌ๋ผ์ด๋”๋ฅผ ๋ณด๊ณ  ๋Œ์–ด๋‹น๊ฒจ ํ•ด๋‹น ํ™”๋ฉด์„ popํ•œ ํ›„์—, ์ œํ’ˆ ๋‚ด ๋ฉ”์ธ ํผ๋„ ์ค‘ ํ•˜๋‚˜์ธ ์š”์ฒญ์„œ ์ž‘์„ฑํ™”๋ฉด์— ์ง„์ž…ํ•œ ๊ฒฝ์šฐ์— ์งˆ๋ฌธ์ด 10๊ฐœ๋งŒ ๋ Œ๋”๋ง๋˜๋Š” ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

์ด๋ฏธ์ง€๋ฅผ ๋Œ์–ด๋‹น๊ฒจ ์ข…๋ฅ˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด PanResponder๋ฅผ ์ด์šฉํ•ด ๊ตฌํ˜„ํ–ˆ๋Š”๋ฐ, PanResponder๋กœ ๋Œ์–ด๋‹น๊ฒจ navigation.pop์„ ์ง„ํ–‰ํ•˜๊ฒŒ ํ•˜๋Š” ๋™์ž‘์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์˜€๋‹ค.

ํ•ด๋‹น ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ณผ์ •์—์„œ FlatList๋กœ ๊ตฌํ˜„๋œ ์š”์ฒญ์„œ ์ž‘์„ฑํ™”๋ฉด์˜ ์š”์†Œ๊ฐ€ Flatlist์˜ initialNumToRender prop์˜ default ๊ฐ’์ฒ˜๋Ÿผ ๋”ฑ 10๊ฐœ๋งŒ ํ•ญ์ƒ ๋ Œ๋”๋ง๋˜๋Š” ๊ฒƒ์„ ๊ทผ๊ฑฐ๋กœ FlatList ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•ด๋ณด๊ฒŒ ๋˜์—ˆ๋‹ค.

Flatlist๋Š” VirtualizedList๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ตฌํ˜„๋˜์–ด ์žˆ๊ณ , VirtualizedList๋Š” ๋‹ค์Œ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด์„œ Batchinator๋ฅผ ์ด์šฉํ•ด ๋‹ค์Œ ๋ชฉ๋ก์„ ๋ฐ›์•„์˜ค๊ฒŒ๋œ๋‹ค. ์ด๋•Œ Batchinator๋Š” InteractionManager์˜ runAfterInteractions ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด ๋‹ค์Œ ๋ชฉ๋ก์„ ๋ฐ›์•„์˜ค๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์—ˆ๋‹ค.

[ Batchinator ์ฝ”๋“œ]

class Batchinator {
  _callback: () => void;
  _delay: number;
  _taskHandle: ?{cancel: () => void, ...};
  constructor(callback: () => void, delayMS: number) {
    this._delay = delayMS;
    this._callback = callback;
  }
  /*
   * Cleanup any pending tasks.
   *
   * By default, if there is a pending task the callback is run immediately. Set the option abort to
   * true to not call the callback if it was pending.
   */
  dispose(options: {abort: boolean, ...} = {abort: false}) {
    if (this._taskHandle) {
      this._taskHandle.cancel();
      if (!options.abort) {
        this._callback();
      }
      this._taskHandle = null;
    }
  }
  schedule() {
    if (this._taskHandle) {
      return;
    }
    const timeoutHandle = setTimeout(() => {
      this._taskHandle = InteractionManager.runAfterInteractions(() => {
        // Note that we clear the handle before invoking the callback so that if the callback calls
        // schedule again, it will actually schedule another task.
        this._taskHandle = null;
        this._callback();
      });
    }, this._delay);
    this._taskHandle = {cancel: () => clearTimeout(timeoutHandle)};
  }
}

module.exports = Batchinator;

์ด๋ฏธ์ง€๋ฅผ ๋Œ์–ด๋‹น๊ฒจ ์ข…๋ฃŒํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” PanResponder ๋˜ํ•œ ๋‚ด๋ถ€ ์ ์œผ๋กœ InteractionManager๋ฅผ ์ด์šฉํ•˜๋Š”๋ฐ ์ด๋•Œ gesture ๋ฐฉํ•ด๋ฅผ ๋ง‰๊ธฐ์œ„ํ•ด JS ์ด๋ฒคํŠธ๋ฅผ blockingํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค.

pan responder ์ฃผ์„
pan responder ์ฃผ์„

์ด๋ฅผ ๊ทผ๊ฑฐ๋กœ ์ž์„ธํ•œ ๋™์ž‘๊ณผ ์ถฉ๋Œ ๊ณผ์ •์€ ํŒŒ์•…ํ•˜์ง€ ๋ชปํ–ˆ์ง€๋งŒ ์ด๋ฏธ์ง€๋ฅผ ๋Œ์–ด๋‹น๊ฒจ ์ข…๋ฃŒํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์—์„œ Interaction Manager๊ฐ€ blocking๋˜์–ด FlatList์˜ InteractionManager๊ฐ€ ๋™์ž‘ํ•˜์ง€ ๋ชปํ•˜๊ฒŒ ๋ง‰์•„์„œ ๋ฐœ์ƒํ•˜๋Š” ์ด์Šˆ๋กœ ์ถ”์ธกํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

ํ•ด๋‹น ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๊ฒŒ ์œ„ํ•ด์„œ Pan Responder์˜ interaction ๋„์ค‘์— navigation.pop์ด ์ง„ํ–‰๋˜๊ฒŒ ํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ Interaction์ด ๋ชจ๋‘ ๋๋‚œ ํ›„์— ์ง„ํ–‰๋˜๊ฒŒ ํ•ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

    const panResponder = useRef(
        PanResponder.create({
            ...,
            onPanResponderGrant: () => {
                // PanGesture๊ฐ€ ์™„๋ฃŒ๋˜๊ณ  ๋‚œ ์ดํ›„์— ํ™”๋ฉด ์ด๋™์„ ๋™์ž‘์‹œ์ผœ InteractionManager๊ฐ„ ์ถฉ๋Œ์„ ๋ง‰์Šต๋‹ˆ๋‹ค.
                InteractionManager.runAfterInteractions(() => {
                        navigation.goBack();
                });
            },
        }),
    ).current;

โญ๏ธ ์ ์šฉ ํ›„ ์„ฑ๋Šฅ ๋ถ„์„

์ด์ œ ์ ์šฉ ํ›„ ์„ฑ๋Šฅ ๋ถ„์„์„ ์ง„ํ–‰ํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด๋ ค ํ•œ๋‹ค. ํ™”๋ฉด ์ „ํ™˜๊ฐ„ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•œ๋‹ค๋Š” ๋ชฉํ‘œ๋ฅผ ๊ฐ€์ง€๊ณ  ์ž‘์—…์„ ์ง„ํ–‰ํ–ˆ๋‹ค ๋ณด๋‹ˆ ์ง€ํ‘œ์ ์œผ๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์—ฌ์ค„ ๋ฐฉ๋ฒ•์ด ํฌ๊ฒŒ ์—†์–ด ์–ด๋ ค์›€์„ ๋Š๊ผˆ๋‹ค.

๊ณ ๋ฏผ ๋์— ์ •ํ–ˆ๋˜ ๋ฐฉ๋ฒ•์€ ์ด 3๊ฐ€์ง€๋กœ, ๋จผ์ € ๊ฐ„๋‹จํ•˜๊ฒŒ ์‹ค์ œ ์ œํ’ˆ ๋‚ด ๋ฉ”์ธ ํผ๋„๋“ค์— ๋Œ€ํ•œ ์˜์ƒ์„ ์ฐ์–ด์„œ ์ „/ํ›„ ๋น„๊ต๋ฅผ ์ง„ํ–‰ํ–ˆ๊ณ , ๋‹ค์Œ์œผ๋กœ ์ง์ ‘์ ์ด์ง€๋Š” ์•Š์ง€๋งŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ ๊ณผ์ •์— ํ•„์š”ํ•œ CPU ์‚ฌ์šฉ๋Ÿ‰๊ณผ memory ์‚ฌ์šฉ๋Ÿ‰์„ ๋ณด๊ธฐ ์œ„ํ•ด android์—์„œ ์›น์˜ light house์ฒ˜๋Ÿผ ์„ฑ๋Šฅ์ธก์ •์„ ํ•  ์ˆ˜ ์žˆ๋Š” flash light์„ ์ด์šฉํ•ด ๋ฉ”์ธ ํผ๋„๋“ค์— ๋Œ€ํ•œ ์ง€ํ‘œ๋ฅผ ์ธก์ •ํ•ด๋ณด์•˜๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ถ”๊ฐ€์ ์œผ๋กœ ํ™”๋ฉด stack์ด ์ตœ๋Œ€ 100๊ฐœ๊ฐ€ ๋˜์—ˆ์„ ๋•Œ๋„ ์ •์ƒ์ ์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์„์ง€ ๋ณด๊ธฐ ์œ„ํ•ด stress ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด์•˜๋‹ค.

์˜์ƒ์„ ํ†ตํ•œ ์ „/ํ›„ ๋น„๊ต

์˜์ƒ์„ ํ†ตํ•œ ์ „/ํ›„ ๋น„๊ต๋Š” ๊ฐ€์žฅ ์ง๊ด€์ ์œผ๋กœ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ์˜์ƒ์„ ์ฐ์–ด์„œ ์ „/ํ›„ ๋น„๊ต๋ฅผ ์ง„ํ–‰ํ–ˆ๊ณ , ์ด๋ฅผ ํ†ตํ•ด ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์ž์—ฐ์Šค๋Ÿฌ์›€๊ณผ ๋Š๊น€์ด ์žˆ๋Š”์ง€, ๋˜ํ•œ ํ™”๋ฉด ์ „ํ™˜ ์†๋„๊ฐ€ ๋นจ๋ผ์กŒ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์•„๋ž˜๋Š” ๋ฉ”์ธ ํผ๋„ ์ค‘ ํ•˜๋‚˜์ธ ๊ณ ๊ฐํ™ˆ -> ๋ฉ”์ธ ์นดํ…Œ๊ณ ๋ฆฌ ๊นŒ์ง€ ๋„˜์–ด๊ฐ€๋Š” ๊ณผ์ •์„ Android (Galaxy22)์—์„œ ์ฐ์€ ์˜์ƒ์ด๋‹ค.

AS-IS TO-BE
modal modal

์˜์ƒ์„ ๋น„๊ตํ•ด๋ณด์•˜์„ ๋•Œ ์ด์ „๋ณด๋‹ค ์กฐ๊ธˆ ๋” ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ „ํ™˜๋˜๊ณ  ํ™”๋ฉด์ „ํ™˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๊ฐ€ ๋นจ๋ผ์ง„ ๊ฒƒ์„ ๋Š๋‚„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

FlashLight๋ฅผ ์ด์šฉํ•œ ์„ฑ๋Šฅ ์ธก์ •

์˜์ƒ์„ ์ฐ๋Š”๋ฐ์—์„œ ๋๋‚ด์ง€ ์•Š๊ณ  ์ตœ๋Œ€ํ•œ ์ˆซ์ž๋กœ, ์ง€ํ‘œ๋กœ ๋ถ„์„ํ•ด๋ณด๋ฉด ์กฐ๊ธˆ ๋” ์ข‹์ง€ ์•Š์„๊นŒ ์ƒ๊ฐํ•ด flash light๋ฅผ ์ด์šฉํ•ด ์„ฑ๋Šฅ ์ธก์ •์„ ์ง„ํ–‰ํ•ด๋ณด์•˜๋‹ค. flash light์™€ maestro e2e ์ž๋™ํ™” ์ฝ”๋“œ๋ฅผ ์ด์šฉํ•ด ์ธก์ •ํ•ด๋ณด์•˜๊ณ , ์•ฑ์„ ์‹œ์ž‘ํ•ด์„œ ์œ„์—์„œ ์˜์ƒ์œผ๋กœ ์ฐ์—ˆ๋˜ ๋ฉ”์ธ ํผ๋„ ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋Œ€ํ•ด ๊ฐ๊ฐ 30๋ฒˆ ์ง„ํ–‰ํ–ˆ์„ ๋•Œ ์„ฑ๋Šฅ ์ง€ํ‘œ๋ฅผ ์ธก์ •ํ–ˆ๋‹ค.

ํ™ˆ -> ๋ฉ”์ธ ์นดํ…Œ๊ณ ๋ฆฌ ์ „/ํ›„
modal
modal
๋ฐ›์€ ์š”์ฒญ ๋ชฉ๋ก -> ๋ฐ›์€ ์š”์ฒญ ์ƒ์„ธ ์ „/ํ›„
modal
modal
๋ฐ›์€ ๊ฒฌ์  ๋ชฉ๋ก -> ๋ฐ›์€ ๊ฒฌ์  ์ƒ์„ธ -> ๊ฒฌ์ ์„œ ์ƒ์„ธ ์ „/ํ›„
modal
modal
์ฑ„ํŒ…๋ฐฉ ๋ชฉ๋ก -> ์ฑ„ํŒ…๋ฐฉ AS-IS
modal
modal

์„ฑ๋Šฅ ์ง€ํ‘œ๋ฅผ ๋ถ„์„ํ•ด๋ณด์•˜์„ ๋•Œ ํผ๋„ ๋ณ„๋กœ ์˜ค๋ฅด๋ฝ ๋‚ด๋ฆฌ๋ฝ ํ•˜๋Š” ๋ถ€๋ถ„๋“ค์ด ์žˆ์ง€๋งŒ, CPU ์‚ฌ์šฉ๋Ÿ‰๊ณผ Memory ์‚ฌ์šฉ๋Ÿ‰์ด ๋Œ€๋ถ€๋ถ„ ๊ฐ์†Œํ–ˆ๊ณ  ์ด๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ ๊ณผ์ •์—์„œ ๋” ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค๊ณ  ๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ถ”๊ฐ€์ ์œผ๋กœ ๋ถ„์„ํ•ด๋ณด์•˜์„ ๋•Œ ๋ฐ›์€๊ฒฌ์  ๋ชฉ๋ก -> ๋ฐ›์€ ๊ฒฌ์  ์ƒ์„ธ -> ๊ฒฌ์ ์„œ ์ƒ์„ธ ์ „ํ™˜์—์„œ ๋‹ค๋ฅธ ํผ๋„๊ณผ ๋‹ค๋ฅด๊ฒŒ ์ „์ฒด์ ์ธ ์ง€ํ‘œ ๋ชจ๋‘ ์ข‹์•„์ง„ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ํผ๋„๊ฐ„ ์ฐจ์ด์ ์„ ๋ณด์•˜์„ ๋•Œ ์ธก์ •ํ–ˆ๋˜ ํผ๋„ ์ค‘ ๊ฐ€์žฅ ๊ธด ํผ๋„์ด์—ˆ๋‹ค๋Š” ์ ์„ ๊ณ ๋ คํ•ด Navigation stack์ด ์Œ“์ผ์ˆ˜๋ก ๋” ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ด๋Ÿฌํ•œ ํŠน์ง•์„ ๋ˆˆ์œผ๋กœ ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ํ™•์ธํ•ด๋ณด๊ธฐ ์œ„ํ•ด์„œ Navigation stack์ด 100๊ฐœ๊ฐ€ ๋˜์—ˆ์„ ๋•Œ๋„ ์„ฑ๋Šฅ์ด ์ข‹์•„์ง€๋Š”์ง€ ํ™•์ธํ•ด๋ณด๋Š” ๊ฑด ์–ด๋–จ๊นŒ๋ผ๋Š” ํ˜ธ๊ธฐ์‹ฌ์ด ์ƒ๊ฒจ stress ํ…Œ์ŠคํŠธ๋ฅผ ์ด์–ด์„œ ์ง„ํ–‰ํ•ด๋ณด์•˜๋‹ค.

Stress ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•œ ์„ฑ๋Šฅ ํ™•์ธ

stress ํ…Œ์ŠคํŠธ๋Š” 10๊ฐœ, 30๊ฐœ, 50๊ฐœ, 75๊ฐœ, 100๊ฐœ ๊นŒ์ง€ stack์— ํ™”๋ฉด์ด ์Œ“์˜€์„ ๋•Œ ์–ด๋–ป๊ฒŒ ํ™”๋ฉด์ „ํ™˜์ด ๋˜๋Š”์ง€๋ฅผ ์ดฌ์˜ํ•ด๋ณด์•˜๊ณ , ์˜์ƒ ์ƒ๋‹จ์— navigation stack์˜ ๊ธธ์ด๋ฅผ ํ‘œ์‹œํ•ด ํ˜„์žฌ ๋ช‡๊ฐœ stack์ด ์Œ“์˜€๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค. ์•„๋ž˜ ์˜์ƒ์€ Android Galaxy22 ๊ธฐ๊ธฐ์—์„œ ์ธก์ •ํ•œ ๊ฒฐ๊ณผ๋‹ค.

Stack Navigator 10๊ฐœ Stack Navigator 50๊ฐœ Stack Navigator 100๊ฐœ
modal modal modal
Native Stack Navigator 10๊ฐœ Native Stack Navigator 50๊ฐœ Native Stack Navigator 100๊ฐœ
modal modal modal

Stack Navigator๋Š” stack์ด ์Œ“์ผ์ˆ˜๋ก ํ™”๋ฉด์ „ํ™˜ ์†๋„๊ฐ€ ๋Š๋ ค์ง€๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ , Native Stack Navigator๋Š” stack์ด ์Œ“์—ฌ๋„ ์œ ์‚ฌํ•œ ์†๋„๋กœ ํ™”๋ฉด์ „ํ™˜์ด ์ง„ํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

์ดฌ์˜๊ณผ์ •์—์„œ ๊ธฐ๊ธฐ์—์„œ ๋Š๊ปด์ง€๋Š” ๋ฐœ์—ด๋„ Native Stack Navigator๊ฐ€ ๋” ๋‚ฎ์€ ๊ฒƒ์„ ๋Š๋‚„ ์ˆ˜ ์žˆ์—ˆ๊ณ , ์ด๋Š” CPU ์‚ฌ์šฉ๋Ÿ‰๊ณผ Memory ์‚ฌ์šฉ๋Ÿ‰์ด ์ค„์–ด๋“ค์–ด ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ์ฒด๊ฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

๐Ÿ“š ๋ฐฐ์šด์ 

์—ฌํƒœ๊นŒ์ง€ ์ง„ํ–‰ํ–ˆ๋˜ ํ”„๋กœ์ ํŠธ ์ค‘์—์„œ ๊ฐ€์žฅ ์ด์Šˆ๊ฐ€ ๋งŽ์•˜๋˜ ์ž‘์—…์ด์—ˆ๊ณ , ๋‹คํ–‰ํžˆ ์ž˜ ํ•ด๊ฒฐํ•ด์„œ ํ˜„์žฌ ์ž˜ ์ œํ’ˆ์— ๋ฐ˜์˜๋˜์–ด ์žˆ์–ด ๋ฟŒ๋“ฏํ–ˆ๋‹ค.

๋‹จ์ˆœ Javascript ๋Ÿฐํƒ€์ž„๋งŒ ๊ณ ๋ฏผํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ๋ชจ๋ฐ”์ผ ํ”Œ๋žซํผ์— ํŠนํ™”๋œ ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์–ด ์ข‹์•˜๊ณ , ์ž˜ ๋งŒ๋“ค์–ด์ง„ ์˜คํ”ˆ ์†Œ์Šค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ๋‚ด๋ถ€๊ตฌ์กฐ๋„ ํŒŒํ—ค์น˜๋Š” ์ข‹์€ ๊ฒฝํ—˜์ด ๋˜์–ด ์ดํ›„์— React Navigation์—๋„ ๊ธฐ์—ฌํ•˜๊ณ  ์‹ถ์€ ๋งˆ์Œ๋„ ์ƒ๊ฒผ๋‹ค.

์ ์šฉํ•˜๋ฉด์„œ ์„ฑ๋Šฅ์ด ๊ฐœ์„ ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด๋ณด์•˜๋Š”๋ฐ, ์˜์ƒ์„ ์ฐ์–ด ์ „/ํ›„ ๋น„๊ต๋ฅผ ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ง๊ด€์ ์ด์—ˆ๋‹ค. ๋‚˜๋ฆ„ ์ˆซ์ž๋กœ ํ‘œํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ๋…ธ๋ ฅํ•ด๋ณด์•˜์ง€๋งŒ ์–ด๋Š์ •๋„ ํ•œ๊ณ„๊ฐ€ ์žˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ, ๋ฉ”๋ชจ๋ฆฌ๋‚˜ CPU ์‚ฌ์šฉ๋Ÿ‰๊ณผ ๊ฐ™์ด ์ด์ „์— ๊ด€์‹ฌ์„ ๋‘์ง€ ์•Š์•˜๋˜ ํ•˜๋“œ์›จ์–ด ์ŠคํŽ™์— ๋Œ€ํ•ด์„œ๋„ ๊ด€์‹ฌ์„ ๊ฐ€์ง€๊ฒŒ ๋˜์—ˆ๊ณ , ์ด๋ฅผ ํ†ตํ•ด ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๋Š”๋ฐ ๋„์›€์ด ๋˜์—ˆ๋‹ค.

๋ฌผ๋ก  ์—ฌ์ „ํžˆ ๋‚จ๊ฒจ์ง„ ์ผ๋“ค์ด ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์˜ ๋˜ ํฐ ์ผ๊ฐ์„ ์ž˜ ๋งˆ๋ฌด๋ฆฌํ•˜๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๊ณผ ์„ฑ๋Šฅ ๋ชจ๋‘ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ์ข‹์€ ์ž‘์—…์ด์—ˆ๋‹ค๊ณ  ์ƒ๊ฐ๋˜์—ˆ๋‹ค.

@Troy
๋งค์ผ์˜ ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๊ฐœ๋ฐœ์ผ์ง€์ž…๋‹ˆ๋‹ค.