Hello Kitty Eyes Shut
๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿ’ป๊ณต๋ถ€ ๊ธฐ๋ก/๐Ÿ“Œ Frontend

[Frontend] React Native์—์„œ ์ž์—ฐ์Šค๋Ÿฌ์šด scroll fade ๊ตฌํ˜„ํ•˜๊ธฐ - MaskedView

๋ฐ˜์‘ํ˜•

๐Ÿ“‘ ๋“ค์–ด๊ฐ€๋ฉฐ

๋™์•„๋ฆฌ์—์„œ React Native๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ

์—ฐ๋„์™€ ์›”์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด์—ˆ๋‹ค.

 

๋””์ž์ธ ์‹œ์•ˆ์€ ์œ„์™€ ๊ฐ™์•˜๋‹ค.

  • ์—ฐ๋„์™€ ์›” ๋ฆฌ์ŠคํŠธ๋Š” ๊ฐ๊ฐ ์„ธ๋กœ๋กœ ์Šคํฌ๋กค๋œ๋‹ค.
  • ํ•œ ํ™”๋ฉด์—์„œ๋Š” ์•ฝ 6๊ฐœ์˜ ํ•ญ๋ชฉ์ด ๋ณด์ธ๋‹ค.
  • ๊ฐ€์žฅ ์•„๋ž˜ ํ•ญ๋ชฉ์€ ์€์€ํ•˜๊ฒŒ ํŽ˜์ด๋“œ์•„์›ƒ ๋˜์–ด์•ผ ํ•œ๋‹ค.

 

๊ทธ๋ž˜์„œ ์ฒ˜์Œ์—” ๋ฆฌ์ŠคํŠธ ์•„๋ž˜์— LinearGradient๋ฅผ ํ•˜๋‚˜ ๊น”๊ณ ,

๊ทธ๋ผ๋ฐ์ด์…˜์œผ๋กœ ๊ฐ€๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ–ˆ์—ˆ๋‹ค.

 

์ƒํ™ฉ์„ ์ข€ ๋” ์ง๊ด€์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด ์ปฌ๋Ÿฌ๊ฐ’์„ ๋ณ€๊ฒฝํ•œ ๊ฒฐ๊ณผ ,,

 

ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ ๊ตฌํ˜„ํ•ด๋ณด๋‹ˆ, ์—ฌ๋Ÿฌ ๋ฌธ์ œ๋“ค์ด ์žˆ์—ˆ๋‹ค ..๐Ÿ˜ญ

  • ๊ธ€์ž๋งŒ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์‚ฌ๋ผ์ง€๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ๋ฐฐ๊ฒฝ๊นŒ์ง€ ํ•จ๊ป˜ ํ๋ ค์ง
  • ์Šคํฌ๋กค์ด ๋๋‚ฌ์„ ๋•Œ, ๋” ๋‚ด๋ ค๊ฐˆ ํ•ญ๋ชฉ์ด ์—†์–ด๋„ ๊ทธ๋ผ๋ฐ์ด์…˜์ด ๊ณ„์† ๋‚จ์•„์žˆ์Œ
    ํŠนํžˆ 1์›”์˜ ๊ฒฝ์šฐ 0์›” ๊ฐ™์€ ๊ฑด ์—†์œผ๋‹ˆ๊นŒ ์ด๊ฒŒ ๋งˆ์ง€๋ง‰ ์š”์†Œ์ผํ…๋ฐ,
    ๊ทธ๋ ‡๋‹ค๋ณด๋‹ˆ ๊ณ„์†ํ•ด์„œ ๊ทธ๋ผ๋ฐ์ด์…˜์ด ๋‚จ์•„์žˆ๊ฒŒ ๋ผ์„œ ๊ธ€์”จ๊ฐ€ ์ž˜ ์•ˆ ๋ณด์˜€๋‹ค .. ๐Ÿ˜“
  • ์„ ํƒ๋œ ํ•ญ๋ชฉ์˜ ์Šคํƒ€์ผ์ด ๋ฐ”๋€Œ์–ด๋„, ๊ทธ๋ผ๋ฐ์ด์…˜์ด ๊ณ„์† ๊ธ€์ž๋ฅผ ๋ฎ์—ฌ๋ฒ„๋ ค์„œ ์ž˜ ๋ณด์ด์ง€๊ฐ€ ์•Š์Œ
    ์–˜๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ 1์›”์ด ๊ฐ€์žฅ ๋ฌธ์ œ์˜€๋Š”๋ฐ ..
    ํ•ด๋‹น ์—ฐ๋„๋‚˜ ์›”์ด ์„ ํƒ๋˜๋ฉด ๊ธ€์ž ์ƒ‰์ด white๋กœ ๋ฐ”๋€Œ๊ณ , bold์ฒด๋กœ ๋ณ€๊ฒฝ๋˜๋Š”๋ฐ,
    1์›”์—” ๊ณ„์†ํ•ด์„œ ๊ทธ๋ผ๋ฐ์ด์…˜์ด ๋‚จ์•„์žˆ๋‹ค ๋ณด๋‹ˆ๊นŒ ์„ ํƒ์ด ๋œ๊ฑด์ง€ ๋งŒ๊ฑด์ง€ ์ž˜ ๋ณด์ด์ง€๊ฐ€ ์•Š์•˜๋‹ค ใ… ใ…กใ… 

 

๋”ฐ๋ผ์„œ LinearGradient๋ฅผ ์ด์šฉํ•œ ๋ฐฉ๋ฒ• ๋ณด๋‹ค๋Š”, ๊ธ€์ž ์ž์ฒด์— ํŽ˜์ด๋“œ๋ฅผ ์ ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค.

๋˜ํ•œ, ๋ฌด์กฐ๊ฑด ๊ฐ€์žฅ ์•„๋ž˜ ์š”์†Œ์— ํŽ˜์ด๋“œ๋ฅผ ์ ์šฉํ•˜๋Š” ๋Œ€์‹ ์—

์•„๋ž˜๋กœ ๋” ์Šคํฌ๋กคํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ํ•˜๋‹จ ํŽ˜์ด๋“œ๋ฅผ onํ•˜๊ณ ,

์œ„๋กœ ๋” ์Šคํฌ๋กคํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ์ƒ๋‹จ ํŽ˜์ด๋“œ๋ฅผ onํ•˜๋Š” ๋ฐฉ์‹์„ ์ ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

 

 


๐ŸŸฅ MaskedView ์ ์šฉํ•˜๊ธฐ

MaskedView๋Š” iOS์™€ Android์—์„œ ๋ชจ๋‘ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ๋กœ,

๋ง๊ทธ๋Œ€๋กœ ๋งˆ์Šคํฌ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž์‹ ์š”์†Œ๊ฐ€ ๋ณด์ผ์ง€ ์‚ฌ๋ผ์งˆ์ง€๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค.

 

์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ ๊ฐœ๋…์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

๋งˆ์Šคํฌ ์ƒ‰์ƒ ์‹ค์ œ ์ฝ˜ํ…์ธ 
black (๋ถˆํˆฌ๋ช…) 100% ๋ณด์ž„
transparent (ํˆฌ๋ช…) 0% ๋ณด์ž„
mid-gray ์ผ๋ถ€๋งŒ ๋ณด์ž„ (= ํŽ˜์ด๋“œ ํšจ๊ณผ)

 

์ฆ‰, ๊ฒ€์€์ƒ‰์ผ์ˆ˜๋ก ๊ธ€์ž๊ฐ€ ๋” ์„ ๋ช…ํ•˜๊ฒŒ ๋ณด์ด๊ณ , ํˆฌ๋ช…ํ• ์ˆ˜๋ก ์‚ฌ๋ผ์ง€๋Š” ๊ฒƒ์ด๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ์š”๊ฑธ ์ž˜ ์กฐํ•ฉํ•˜๋ฉด, ์Šคํฌ๋กค๋˜๋Š” ํ…์ŠคํŠธ๊ฐ€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์‚ฌ๋ผ์ง€๋Š” UI๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•˜๋‹ค !! ๐Ÿคฉ

 

 


๐ŸŸง Mask ๊ตฌ์กฐ ์„ค๊ณ„

๋‚˜๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ๋งˆ์Šคํฌ๋ฅผ 3๊ฐœ์˜ ์˜์—ญ์œผ๋กœ ๋‚˜๋ˆ ์„œ ๊ตฌ์„ฑํ–ˆ๋‹ค.

mask (listContainer์™€ ๋™์ผ ๋†’์ด)
 โ”œโ”€โ”€ topMask (์œ„ ํŽ˜์ด๋“œ ๊ตฌ๊ฐ„)
 โ”œโ”€โ”€ middleMask (์ •์ƒ์ ์œผ๋กœ ๋ณด์ด๋Š” ๊ตฌ๊ฐ„)
 โ””โ”€โ”€ bottomMask (์•„๋ž˜ ํŽ˜์ด๋“œ ๊ตฌ๊ฐ„)

 

์ด๋ ‡๊ฒŒ 3๋‹จ ๊ตฌ์กฐ๋กœ ๋‚˜๋ˆˆ ์ด์œ ๋Š” ๊ฐ„๋‹จํ•˜๋‹ค.

  • topMask
    • ๋” ์œ„๋กœ ์Šคํฌ๋กคํ•  ์š”์†Œ๊ฐ€ ๋‚จ์•„์žˆ์„ ๋•Œ๋งŒ ํŽ˜์ด๋“œ๋ฅผ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค.
    • ์ฆ‰, ๋ฆฌ์ŠคํŠธ์˜ ์ตœ์ƒ๋‹จ์— ๋‹ฟ์œผ๋ฉด ํ•„์š” ์—†์œผ๋ฏ€๋กœ off ํ•œ๋‹ค.
  • middleMask
    • ๊ฐ€์šด๋ฐ 4๊ฐœ ์š”์†Œ๋Š” ํ•ญ์ƒ fully visibleํ•œ ์˜์—ญ์ด๋ฏ€๋กœ ์„ ๋ช…ํ•˜๊ฒŒ ๋ณด์ด๋„๋ก ํ•ด์•ผ ํ•œ๋‹ค.
  • bottomMask
    • ์•„๋ž˜๋กœ ๋” ์Šคํฌ๋กคํ•  ์š”์†Œ๊ฐ€ ๋‚จ์•„์žˆ์„ ๋•Œ๋งŒ ํŽ˜์ด๋“œ๋ฅผ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค.
    • ์ฆ‰, ๋ฆฌ์ŠคํŠธ์˜ ์ตœํ•˜๋‹จ์— ๋‹ฟ์œผ๋ฉด ํ•„์š” ์—†์œผ๋ฏ€๋กœ off ํ•œ๋‹ค.

์ด์ฒ˜๋Ÿผ ๋งˆ์Šคํฌ ์ž์ฒด๋ฅผ ๋™์ ์œผ๋กœ ์กฐ์ ˆํ•ด์„œ

์—ฐ๋„์™€ ์›” ๋ฆฌ์ŠคํŠธ์˜ ์œ„์•„๋ž˜์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์‚ฌ๋ผ์ง€๋Š” ํšจ๊ณผ๊ฐ€ ์ƒ๊ธฐ๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค.

 

 


๐ŸŸจ ๊ตฌํ˜„ ๊ณผ์ •

MaskedView ์„ค์น˜

npm install @react-native-masked-view/masked-view

 

 

์ƒํƒœ๊ฐ’์œผ๋กœ fade ์—ฌ๋ถ€ ์ œ์–ด

์Šคํฌ๋กค์ด ์œ„์•„๋ž˜๋กœ ๋” ๊ฐ€๋Šฅํ•œ์ง€์— ๋”ฐ๋ผ ๊ฐ ๋ฐฉํ–ฅ ํŽ˜์ด๋“œ๋ฅผ ์ผœ๊ฑฐ๋‚˜ ๋„๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.

const [showMonthTopFade, setShowMonthTopFade] = useState(false);
const [showMonthBottomFade, setShowMonthBottomFade] = useState(true);

 

์ฆ‰, ๋” ์œ„๋กœ ์Šคํฌ๋กค์ด ๊ฐ€๋Šฅํ•˜๋ฉด topFade = true๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด๊ณ ,

๋”์ด์ƒ ์œ„๋กœ ์Šคํฌ๋กคํ•  ๊ฒŒ ์—†์œผ๋ฉด topFade = false๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

 

์Šคํฌ๋กค ์ด๋ฒคํŠธ์—์„œ ํŽ˜์ด๋“œ ํ‘œ์‹œ ์—ฌ๋ถ€ ํŒ๋‹จ

์Šคํฌ๋กคํ•  ๋•Œ๋งˆ๋‹ค ํ˜„์žฌ ์œ„์น˜๋ฅผ ์ธก์ •ํ•ด์„œ fade๋ฅผ on / off ํ•ด์ฃผ๋„๋ก ํ•˜์˜€๋‹ค.

const handleMonthScroll = (e) => {
  const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
  const y = contentOffset.y;
  const maxScroll = contentSize.height - layoutMeasurement.height;

  setShowMonthTopFade(y > 0); // y๊ฐ€ 0 ๋ณด๋‹ค ํฌ๋ฉด ์œ„๋กœ ์Šคํฌ๋กคํ•  ์—ฌ์œ ๊ฐ€ ์žˆ๋Š” ๊ฑฐ๋‹ˆ๊นŒ fade on!
  setShowMonthBottomFade(y < maxScroll); // y๊ฐ€ maxScroll ๋ณด๋‹ค ์ž‘์œผ๋ฉด ์•„์ง ์Šคํฌ๋กคํ•  ์—ฌ์œ ๊ฐ€ ์žˆ๋Š” ๊ฑฐ๋‹ˆ๊นŒ fade on!
};

 

์ด ๋กœ์ง ๋•๋ถ„์— ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ ๋ฐฉํ–ฅ์—๋งŒ fade๊ฐ€ ์ ์šฉ๋ผ์„œ UX๊ฐ€ ๋งค์šฐ ์ž์—ฐ์Šค๋Ÿฌ์›Œ์กŒ๋‹ค ๐Ÿ‘๐Ÿป

 

 

MaskedView์˜ ๋งˆ์Šคํฌ ์š”์†Œ ๋งŒ๋“ค๊ธฐ

{/* ์›” ์นผ๋Ÿผ */}
<View style={styles.column}>
  <MaskedView
    style={styles.listContainer}
    maskElement={
      <View style={styles.mask}>
        {/* ์œ„์ชฝ fade */}
        {showMonthTopFade && (
          <LinearGradient
            colors={["rgba(0,0,0,0)", "rgba(0,0,0,1)"]}
            style={styles.topMask}
          />
        )}

        {/* ์Šคํฌ๋กค ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋˜๋ ทํ•˜๊ฒŒ ๋ณด์ด๋Š” ์˜์—ญ */}
        <View style={styles.middleMask} />

        {/* ์•„๋ž˜์ชฝ fade */}
        {showMonthBottomFade && (
          <LinearGradient
            colors={["rgba(0,0,0,1)", "rgba(0,0,0,0)"]}
            style={styles.bottomMask}
          />
        )}
      </View>
    }
  >
    {/* ์—ฌ๊ธฐ ์•ˆ์— ์‹ค์ œ month ๋ฆฌ์ŠคํŠธ ScrollView */}
  </MaskedView>
</View>

 

mask: {
  flex: 1, // ๋งˆ์Šคํฌ ์ „์ฒด ๋†’์ด๋ฅผ ๋ฆฌ์ŠคํŠธ ์˜์—ญ ์ „์ฒด์™€ ๋˜‘๊ฐ™์ด ๋งž์ถฐ์คŒ
},
topMask: {
  height: ITEM_HEIGHT * 1.5, // ์œ„์ชฝ์—์„œ ์„œ์„œํžˆ ๋‚˜ํƒ€๋‚˜๋Š” fade ๊ตฌ๊ฐ„
},
middleMask: {
  flex: 1,
  backgroundColor: "black", // ๊ฐ€์šด๋ฐ ์˜์—ญ์€ ํ•ญ์ƒ ์™„์ „ํžˆ ๋ณด์ด๋„๋ก ๋งˆ์Šคํฌ๋ฅผ black์œผ๋กœ!
},
bottomMask: {
  height: ITEM_HEIGHT * 1.5, // ์•„๋ž˜๋กœ ๊ฐˆ ์ˆ˜๋ก ์„œ์„œํžˆ ์‚ฌ๋ผ์ง€๋Š” fade
},

 

 


๐ŸŸฉ ๊ฒฐ๊ณผ

 

๊ฐ€์žฅ ์™ผ์ชฝ์€ ๋”์ด์ƒ ์œ„๋กœ ์Šคํฌ๋กคํ•  ๊ฒŒ ์—†๋Š” ๊ฒฝ์šฐ ํ…์ŠคํŠธ์— fade๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๋Š” ๋ชจ์Šต (12์›”),

๊ฐ€์šด๋ฐ๋Š” ์œ„์•„๋ž˜ ๋ชจ๋‘ ์Šคํฌ๋กคํ•  ๊ฒŒ ์žˆ๋Š” ๊ฒฝ์šฐ ๊ฐ€์žฅ ์œ„์™€ ๊ฐ€์žฅ ์•„๋ž˜ ํ…์ŠคํŠธ์— fade๊ฐ€ ์ ์šฉ๋œ ๋ชจ์Šต (10์›”๊ณผ 5์›”),

๊ฐ€์žฅ ์˜ค๋ฅธ์ชฝ์€ ๋”์ด์ƒ ์•„๋ž˜๋กœ ์Šคํฌ๋กคํ•  ๊ฒŒ ์—†๋Š” ๊ฒฝ์šฐ ํ…์ŠคํŠธ์— fade๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๋Š” ๋ชจ์Šต์ด๋‹ค (1์›”).

๋ฐ˜์‘ํ˜•