Upgrade to Pro — share decks privately, control downloads, hide ads and more …

フロントエンドもJetpack Composeで書きたい! -Compose for WebはモダンWebアプリケーションの夢を見るか?-

subroh_0508
December 10, 2022

フロントエンドもJetpack Composeで書きたい! -Compose for WebはモダンWebアプリケーションの夢を見るか?-

Kotlin Fest 2022 Track B 14:05〜
「フロントエンドもJetpack Composeで書きたい! -Compose for WebはモダンWebアプリケーションの夢を見るか?-」のセッション資料です

Proposal: https://fortee.jp/kotlin-fest-2022/proposal/de1b3085-8d72-4f21-a3e1-b0ede9a847fa

subroh_0508

December 10, 2022
Tweet

More Decks by subroh_0508

Other Decks in Technology

Transcript

  1. ໨࣍  ,PUMJO+4ͷ֓ཁ  ݴޠ͔Βఏڙ͞ΕΔػೳͷ঺հ  ,PUMJO+4ʹΑΔ8FCΞϓϦέʔγϣϯ࣮૷ͷ֓ཁղઆ ͭ  

    $PNQPTFGPS8FCʹΑΔ8FCΞϓϦέʔγϣϯ։ൃ  ࣄྫɾ࣮૷ͷղઆ  ,PUMJO+4Ͱͷ8FCϑϩϯτΤϯυ։ൃͷ௕ॴɾ୹ॴɺࠓޙͷల๬ 
  2. 8FCϑϩϯτΤϯυ։ൃɺͦ΋ͦ΋Կ͕ඞཁʁ  ˒)5.-$448FCϖʔδͷݟͨ໨Λఆٛ͢Δ ˒+BWB4DSJQUίʔυ8FCϖʔδʹಈ͖Λ͚ͭΔ 㾎ύοέʔδ؅ཧπʔϧ OQN:BSO  㾎Ϟδϡʔϧόϯυϥ 8FCQBDL3PMMVQFTCVJMEFUD 

     ෳ਺ͷ+4ϞδϡʔϧΛͭʹ·ͱΊΔ  ϞδϡʔϧؒͷґଘੑղܾɺNJOJGZɺίʔυɾϦιʔεͷม׵ ͜ͷ͕ͭ͋Ε͹ ࠷௿ݶ0,🙆 ྫ5ZQF4DSJQUˠ+BWB4DSJQUɺ4BTTˠ$44 Ϟμϯ͔ͭͦΕͳΓͷن໛ͷ 8FCΞϓϦ։ൃʹ͸ ΄΅ඞਢ☝
  3. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  %0.Λ௚઀ૢ࡞  ,PUMJOͷඪ४ϥΠϒϥϦ͔Β8FC"1*Λར༻͠ɺ%0.Λ௚઀ૢ࡞͢Δ fun main() { val div

    = document.createElement("div") div.innerHTML = "Hello, World!" document.body?.appendChild(div) } TBNQMFLU
  4. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  %0.Λ௚઀ૢ࡞  ,PUMJOͷඪ४ϥΠϒϥϦ͔Β8FC"1*Λར༻͠ɺ%0.Λ௚઀ૢ࡞͢Δ DSFBUF&MFNFOUϝιουͰEJWཁૉΛੜ੒ ੜ੒ͨ͠EJWཁૉ಺ʹจࣈྻΛ௥Ճ )5.-ͷCPEZʹੜ੒ͨ͠EJWཁૉΛ௥Ճ function main()

    { const div = document.createElement('div'); div.innerHTML = 'Hello, World!'; document.body.appendChild(div); } fun main() { val div = document.createElement("div") div.innerHTML = "Hello, World!" document.body?.appendChild(div) } TBNQMFLU TBNQMFKT ಉ͡ಈ࡞Λ͢Δ+BWB4DSJQUίʔυ ,PUMJOίʔυͱͦ͜·Ͱେ͖ͳࠩ͸ͳ͠ ?.͕ͳ͍͘Β͍ 
  5. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  LPUMJOSFBDU  ,PUMJOͷίʔυ͔Β3FBDU"1*Λ࣮ߦ͠ɺ6*Λ࣮૷͢Δ fun main() { val container

    = document.createElement("div") document.body!!.appendChild(container) createRoot(container).render(HelloWorld.create()) } val HelloWorld = FC<Props> { div { +"Hello, World!" } } TBNQMFLU 3FBDU 🚀8FCΞϓϦͷ6*Λએݴతʹ࣮૷ 🚀044Խ͔Β೥͕ܦաɺػೳ͕ચ࿅ ৘ใɾϥΠϒϥϦࢿ࢈΋๛෋ 🚀,PUMJOͷϥούʔϥΠϒϥϦΛ +FU#SBJOT͕ެࣜʹఏڙɺ͜ΕΛར༻ LPUMJOSFBDU+FU#SBJOTLPUMJOXSBQQFSTLPUMJOSFBDU
  6. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  LPUMJOSFBDU  ,PUMJOͷίʔυ͔Β3FBDU"1*Λ࣮ߦ͠ɺ6*Λ࣮૷͢Δ fun main() { val container

    = document.createElement("div") document.body!!.appendChild(container) createRoot(container).render(HelloWorld.create()) } val HelloWorld = FC<Props> { div { +"Hello, World!" } } TBNQMFLU 3FBDU 🚀8FCΞϓϦͷ6*Λએݴతʹ࣮૷ 🚀044Խ͔Β೥͕ܦաɺػೳ͕ચ࿅ ৘ใɾϥΠϒϥϦࢿ࢈΋๛෋ 🚀,PUMJOͷϥούʔϥΠϒϥϦΛ +FU#SBJOT͕ެࣜʹఏڙɺ͜ΕΛར༻ 3FBDUίϯϙʔωϯτͷੜ੒Օॴ
  7. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  LPUMJOSFBDU 🙆3FBDU)PPLTΛར༻͠ɺঢ়ଶ؅ཧͷϩδοΫΛεϚʔτʹهड़Ͱ͖Δ val Counter = FC<Props> { var

    (count, setCount) = useState { 0 } button { onClick = { setCount(count += 1) } +"Count: $count" } } $PVOUFSLU useStateϝιουͰΫϦοΫ਺Λอ࣋͢Δม਺ͱ ม਺Λߋ৽͢ΔͨΊͷؔ਺Λએݴ CVUUPOཁૉͷonClickଐੑͰ ΫϦοΫ਺ߋ৽ͷॲཧΛ࣮ߦ ΫϦοΫ਺ͷදࣔ
  8. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  LPUMJOSFBDU 🙆3FBDU)PPLTΛར༻͠ɺঢ়ଶ؅ཧͷϩδοΫΛεϚʔτʹهड़Ͱ͖Δ val Counter = FC<Props> { var

    (count, setCount) = useState { 0 } button { onClick = { setCount(count += 1) } +"Count: $count" } } $PVOUFSLU const Counter = () => { const [count, setCount] = useState(0); return ( <button onClick={ () => setCount(count + 1) }> `Count: ${count}` </button> ); } $PVOUFSKTY
  9. LPUMJOSFBDU 🙆3FBDU)PPLTΛར༻͠ɺঢ়ଶ؅ཧͷϩδοΫΛεϚʔτʹهड़Ͱ͖Δ const Counter = () => { const [count,

    setCount] = useState(0); return ( <button onClick={ () => setCount(count + 1) }> `Count: ${count}` </button> ); } $PVOUFSKTY ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  val Counter = FC<Props> { var (count, setCount) = useState { 0 } button { onClick = { setCount(count += 1) } +"Count: $count" } } $PVOUFSLU +49ه๏ɾ'VODUJPOBM$PNQPOFOUͷએݴΛ ΠΠײ͡ʹ࠶ݱ😎
  10. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  LPUMJOSFBDU 🙆ஶ໊ͳϥΠϒϥϦͷҰ෦͸+FU#SBJOT͔Βϥούʔ͕ఏڙ͞Ε͍ͯΔ import mui.material.Button import mui.material.ButtonVariant val Counter

    = FC<Props> { val (count, setCount) = useState { 0 } Button { variant = ButtonVariant.contained onClick = { setCount(count + 1) } +"Count: $count" } } ઌఔͷΧ΢ϯλʔΞϓϦΛLPUMJONVJͰ࠶࣮૷ CVUUPOཁૉΛ.6*ͷ#VUUPOίϯϙʔωϯτʹࠩ͠ସ͑
  11. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  LPUMJOSFBDU 🙆ஶ໊ͳϥΠϒϥϦͷҰ෦͸+FU#SBJOT͔Βϥούʔ͕ఏڙ͞Ε͍ͯΔ import mui.material.Button import mui.material.ButtonVariant val Counter

    = FC<Props> { val (count, setCount) = useState { 0 } Button { variant = ButtonVariant.contained onClick = { setCount(count + 1) } +"Count: $count" } } ৭ɾؙ֯ɾେจࣈԽɾ3JQQMF& ff FDU௥ՃFUD .6*ͷػೳʹΑͬͯϦονͳݟͨ໨ʹ✨
  12. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  $PNQPTFGPS8FCͷઆ໌ͷલʹʜ +FUQBDL$PNQPTFʹ͍ͭͯ  "OESPJEΞϓϦͷ6*Λએݴతʹ࣮૷Ͱ͖Δ,PUMJO੡6*ϑϨʔϜϫʔΫ  ೥݄ʹਖ਼ࣜ൛ϦϦʔε  "OESPJEΞϓϦͷ6*։ൃͰ͸

    σϑΝΫτʹͳΓͭͭ͋Δ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Column { Text("Android") Text("Jetpack Compose") } } } } .BJO"DUJWJUZLU
  13. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  $PNQPTFGPS8FCͷઆ໌ͷલʹʜ +FUQBDL$PNQPTFʹ͍ͭͯ  "OESPJEΞϓϦͷ6*Λએݴతʹ࣮૷Ͱ͖Δ,PUMJO੡6*ϑϨʔϜϫʔΫ  ೥݄ʹਖ਼ࣜ൛ϦϦʔε  "OESPJEΞϓϦͷ6*։ൃͰ͸

    σϑΝΫτʹͳΓͭͭ͋Δ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Column { Text("Android") Text("Jetpack Compose") } } } } .BJO"DUJWJUZLU +FUQBDL$PNQPTFͰ࣮૷͞Ε͍ͯΔՕॴ ͭͷจࣈྻΛॎฒͼͰදࣔ
  14. ,PUMJO+4ʹΑΔ8FCϑϩϯτΤϯυ࣮૷  $PNQPTFGPS8FCͷઆ໌ͷલʹʜ +FUQBDL$PNQPTFʹ͍ͭͯ  "OESPJEΞϓϦͷ6*Λએݴతʹ࣮૷Ͱ͖Δ,PUMJO੡6*ϑϨʔϜϫʔΫ  ೥݄ʹਖ਼ࣜ൛ϦϦʔε  "OESPJEΞϓϦͷ6*։ൃͰ͸

    σϑΝΫτʹͳΓͭͭ͋Δ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Column { Text("Android") Text("Jetpack Compose") } } } } .BJO"DUJWJUZLU +FUQBDL$PNQPTFͰ࣮૷͞Ε͍ͯΔՕॴ ͭͷจࣈྻΛॎฒͼͰදࣔ 8FCΞϓϦ΋+FUQBDL$PNQPTFͰ ॻ͚ͨΒ࠷ߴͳͷͰ͸ʜʁ ఱ࠽ͷൃ૝ 
  15. 🌏*OUFMMJ+*%&"$&Ͱ਽ܗ࡞੒ $PNQPTFGPS8FCͰ؆୯ͳΞϓϦΛ࡞Δ  ˒ੜ੒෺ .BJOLU  6*Λهड़͢Δ,PUMJOίʔυ JOEFYIUNM  +4ม׵ޙͷ,PUMJOίʔυΛϒϥ΢βʹಡΈࠐ·ͤΔ

     $44ϞδϡʔϧͷಡΈࠐΈ΍NFUBλάͷهड़ TFUUJOHTHSBEMFLUTCVJMEHSBEMFLUT  ϥΠϒϥϦͷґଘؔ܎ɾ8FCQBDLͷઃఆͷهड़ HSBEMFQSPQFSUJFT  (SBEMFͷઃఆɾϥΠϒϥϦͷόʔδϣϯͷهड़ ੜ੒෺ʹର͠ɺجຊతʹ͸ ௥Ճɾमਖ਼ͷهड़͸ෆཁ
  16. 🌏*OUFMMJ+*%&"$&Ͱ਽ܗ࡞੒ $PNQPTFGPS8FCͰ؆୯ͳΞϓϦΛ࡞Δ  kotlin.version=1.7.20 compose.version=1.2.1 ,PUMJOͱ$PNQPTFͷόʔδϣϯ͸ ࠷৽ʹ͓ͯ͘͠ͱ޾ͤ👍 HSBEMFQSPQFSUJFT ˞Ҏલͷ,PUMJO+4͕ݹ͍/PEFKTʹґଘ ɹ"QQMF4JMJDPOͷ.BDͰϏϧυ͕མͪΔ🥺

    ˒ੜ੒෺ .BJOLU  6*Λهड़͢Δ,PUMJOίʔυ JOEFYIUNM  +4ม׵ޙͷ,PUMJOίʔυΛϒϥ΢βʹಡΈࠐ·ͤΔ  $44ϞδϡʔϧͷಡΈࠐΈ΍NFUBλάͷهड़ TFUUJOHTHSBEMFLUTCVJMEHSBEMFLUT  ϥΠϒϥϦͷґଘؔ܎ɾ8FCQBDLͷઃఆͷهड़ HSBEMFQSPQFSUJFT  (SBEMFͷઃఆɾϥΠϒϥϦͷόʔδϣϯͷهड़
  17.  fun main() { var count: Int by mutableStateOf(0) renderComposable(rootElementId

    = "root") { Div({ style { padding(25.px) } }) { Button(attrs = { onClick { count -= 1 } }) { Text("-") } Span({ style { padding(15.px) } }) { Text("$count") } Button(attrs = { onClick { count += 1 } }) { Text("+") } } } } .BJOLU 🛠࣮૷ৄࡉ
  18.  fun main() { var count: Int by mutableStateOf(0) renderComposable(rootElementId

    = "root") { Div({ style { padding(25.px) } }) { Button(attrs = { onClick { count -= 1 } }) { Text("-") } Span({ style { padding(15.px) } }) { Text("$count") } Button(attrs = { onClick { count += 1 } }) { Text("+") } } } } .BJOLU 🛠࣮૷ৄࡉ mutableStateOfϝιουͰ஋ͷঢ়ଶ؂ࢹ͕ Մೳͳม਺Λఆٛ ˞ͨͩͷ*OUͩͱɺ6*ͷ࠶ඳը͕࣮ߦ͞Εͳ͍
  19.  fun main() { var count: Int by mutableStateOf(0) renderComposable(rootElementId

    = "root") { Div({ style { padding(25.px) } }) { Button(attrs = { onClick { count -= 1 } }) { Text("-") } Span({ style { padding(15.px) } }) { Text("$count") } Button(attrs = { onClick { count += 1 } }) { Text("+") } } } } .BJOLU 🛠࣮૷ৄࡉ id="root"ͷ)5.-ཁૉʹ $PNQPTFͰߏஙͨ͠6*Λඳը͢ΔΑ͏ʹࢦఆ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Sample</title> </head> <body> <div id="root"></div> <script src="kotlinfest2022-compose-for-web-sample.js"></script> </body> </html> JOEFYIUNM
  20.  fun main() { var count: Int by mutableStateOf(0) renderComposable(rootElementId

    = "root") { Div({ style { padding(25.px) } }) { Button(attrs = { onClick { count -= 1 } }) { Text("-") } Span({ style { padding(15.px) } }) { Text("$count") } Button(attrs = { onClick { count += 1 } }) { Text("+") } } } } .BJOLU 🛠࣮૷ৄࡉ $PNQPTFʹΑΔ6*ͷߏஙՕॴ )5.-λάΛ+FUQBDL$PNQPTFͷจ๏ʹԊͬͯ એݴతʹهड़͢Δ
  21.  fun main() { var count: Int by mutableStateOf(0) renderComposable(rootElementId

    = "root") { Div({ style { padding(25.px) } }) { Button(attrs = { onClick { count -= 1 } }) { Text("-") } Span({ style { padding(15.px) } }) { Text("$count") } Button(attrs = { onClick { count += 1 } }) { Text("+") } } } } .BJOLU 🛠࣮૷ৄࡉ 6*ͷݟͨ໨͸$44Ͱهड़͢Δ $PNQPTFGPS8FCʹ.PEJ fi FS͸ొ৔͠ͳ͍🙅  $44͸$PNQPTFGPS8FC͕%4-Λఏڙ ܕ҆શ͔ͭαδΣετΛޮ͔ͤͳ͕Βهड़👍
  22.  fun main() { var count: Int by mutableStateOf(0) renderComposable(rootElementId

    = "root") { Div({ style { padding(25.px) } }) { Button(attrs = { onClick { count -= 1 } }) { Text("-") } Span({ style { padding(15.px) } }) { Text("$count") } Button(attrs = { onClick { count += 1 } }) { Text("+") } } } } .BJOLU 🛠࣮૷ৄࡉ )5.-λάͷଐੑ஋΁ͷ஋୅ೖɾϝιουઃఆ͸ attrsҾ਺ʹϥϜμࣜͷܗͰ౉͢ countม਺͕.VUBCMF4UBUFܕͰએݴ͞Ε͍ͯΔ countͷ஋͕ߋ৽͞ΕΔͱ6*ͷ࠶ඳը͕ࣗಈతʹ૸Δ🏃
  23.  🛠NBUFSJBMJ[FDTTͰαϯϓϧΞϓϦͷݟͨ໨Λվ଄ <head> <!-- 略 --> <!-- materialize.cssのminifyされたCSSファイル --> <link

    rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"> <!-- materialize.cssのminifyされたJSファイル --> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> <!-- Material Iconのフォントファイル --> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> </head> JOEFYIUNM  ˞ৄ͘͠͸NBUFSJBMJ[FDTTͷެࣜυΩϡϝϯτΛࢀরNBUFSJBMJ[FDTTDPN JOEFYIUNMʹ$%/΁ͷϦϯΫΛ௥Ճ NBUFSJBMJ[FDTTͷ$44+4ϑΝΠϧΛಡΈࠐΈ💿
  24.  🛠NBUFSJBMJ[FDTTͰαϯϓϧΞϓϦͷݟͨ໨Λվ଄  ˞ৄ͘͠͸NBUFSJBMJ[FDTTͷެࣜυΩϡϝϯτΛࢀরNBUFSJBMJ[FDTTDPN @Composable fun FloatingActionButton( icon: String, attrs:

    AttrBuilderContext<HTMLButtonElement>? = null, ) = Button(attrs = { classes("btn-floating", "waves-effect", "waves-light") attrs?.invoke(this) }) { I(attrs = { classes("material-icons") }) { Text(icon) } } 'MPBUJOH"DUJPO#VUUPOLU 'MPBUJOH"DUJPO#VUUPO༻ͷ$PNQPTBCMFؔ਺Λ௥Ճ
  25.  🛠NBUFSJBMJ[FDTTͰαϯϓϧΞϓϦͷݟͨ໨Λվ଄  ˞ৄ͘͠͸NBUFSJBMJ[FDTTͷެࣜυΩϡϝϯτΛࢀরNBUFSJBMJ[FDTTDPN @Composable fun FloatingActionButton( icon: String, attrs:

    AttrBuilderContext<HTMLButtonElement>? = null, ) = Button(attrs = { classes("btn-floating", "waves-effect", "waves-light") attrs?.invoke(this) }) { I(attrs = { classes("material-icons") }) { Text(icon) } } 'MPBUJOH"DUJPO#VUUPOLU 'MPBUJOH"DUJPO#VUUPO༻ͷ$PNQPTBCMFؔ਺Λ௥Ճ CVUUPOλάͷଐੑ஋Λઃఆ͢ΔϥϜμࣜ ΫϦοΫϦεφʔ΍$44ͷ্ॻ͖Λ֎෦͔ΒͰ͖ΔΑ͏ʹ typealias AttrBuilderContext<T> = AttrsScope<T>.() -> Unit
  26.  🛠NBUFSJBMJ[FDTTͰαϯϓϧΞϓϦͷݟͨ໨Λվ଄  ˞ৄ͘͠͸NBUFSJBMJ[FDTTͷެࣜυΩϡϝϯτΛࢀরNBUFSJBMJ[FDTTDPN @Composable fun FloatingActionButton( icon: String, attrs:

    AttrBuilderContext<HTMLButtonElement>? = null, ) = Button(attrs = { classes("btn-floating", "waves-effect", "waves-light") attrs?.invoke(this) }) { I(attrs = { classes("material-icons") }) { Text(icon) } } 'MPBUJOH"DUJPO#VUUPOLU 'MPBUJOH"DUJPO#VUUPO༻ͷ$PNQPTBCMFؔ਺Λ௥Ճ attrsϒϩοΫ಺ͷclassesϝιουͰ $44Ϋϥε໊Λઃఆ
  27. fun main() { var count: Int by mutableStateOf(0) renderComposable(rootElementId =

    "root") { Div({ style { display(DisplayStyle.Flex) padding(25.px) } }) { FloatingActionButton("remove") { onClick { count -= 1 } } Span({ style { padding(12.px) } }) { Text("$count") } FloatingActionButton("add") { onClick { count += 1 } } } } }  🛠NBUFSJBMJ[FDTTͰαϯϓϧΞϓϦͷݟͨ໨Λվ଄  ˞ৄ͘͠͸NBUFSJBMJ[FDTTͷެࣜυΩϡϝϯτΛࢀরNBUFSJBMJ[FDTTDPN αϯϓϧΞϓϦͷϘλϯඳըՕॴΛ ௥Ճͨ͠'MPBUJOH"DUJPO#VUUPOʹஔ͖׵͑ .BJOLU
  28. fun main() { var count: Int by mutableStateOf(0) renderComposable(rootElementId =

    "root") { Div({ style { display(DisplayStyle.Flex) padding(25.px) } }) { FloatingActionButton("remove") { onClick { count -= 1 } } Span({ style { padding(12.px) } }) { Text("$count") } FloatingActionButton("add") { onClick { count += 1 } } } } }  🛠NBUFSJBMJ[FDTTͰαϯϓϧΞϓϦͷݟͨ໨Λվ଄  ˞ৄ͘͠͸NBUFSJBMJ[FDTTͷެࣜυΩϡϝϯτΛࢀরNBUFSJBMJ[FDTTDPN .BJOLU Ϙλϯͷݟͨ໨͕Ϧονʹ🎉 3JQQMF& ff FDU΋ͭͧ͘💪
  29.  🧗$44ͷಡΈࠐΈ Ԡ༻ฤ   8FCQBDLͷػೳΛར༻ͯ͠$44ΛಡΈࠐΉ͜ͱ΋Մೳ  ˞8FCQBDLʹ͍ͭͯਂງΓ͍ͨਓ͸ެࣜυΩϡϝϯτΛࢀরXFCQBDLKTPSH CVJMEHSBEMFLUTͱಉ͡֊૚ʹ XFCQBDLDPO

    fi HEͱ͍͏σΟϨΫτϦΛ࡞੒ XFCQBDLDPO fi HEҎԼͷ+4ϑΝΠϧͰ ௨ৗͷ8FCϑϩϯτ։ൃͱಉ͡Α͏ʹ 8FCQBDLͷઃఆΛࣗ༝ʹهड़͢Δ͜ͱ͕Մೳ😆
  30.  🧗$44ͷಡΈࠐΈ Ԡ༻ฤ   8FCQBDLͷػೳΛར༻ͯ͠$44ΛಡΈࠐΉ͜ͱ΋Մೳ  ˞8FCQBDLʹ͍ͭͯਂງΓ͍ͨਓ͸ެࣜυΩϡϝϯτΛࢀরXFCQBDLKTPSH const sass

    = require('sass'); config.entry.main.push(path.resolve(rootPath, 'web/app/src/jsMain/resources/app.scss')); config.module.rules.push({ test: /\.scss$/, use: [ { loader: 'sass-loader', options: { implementation: sass, webpackImporter: false, sassOptions: { includePaths: [nodeModulePath], }, }, }, ], }); XFCQBDLDPO fi HETBTTKT 4BTTͰهड़ͨ͠ελΠϧγʔτΛಡΈࠐΈ $44ʹίϯύΠϧ͢ΔͨΊʹඞཁͳઃఆ
  31. 🧗$44ͷಡΈࠐΈ Ԡ༻ฤ   8FCQBDLͷػೳΛར༻ͯ͠$44ΛಡΈࠐΉ͜ͱ΋Մೳ const sass = require('sass'); config.entry.main.push(path.resolve(rootPath,

    'web/app/src/jsMain/resources/app.scss')); config.module.rules.push({ test: /\.scss$/, use: [ { loader: 'sass-loader', options: { implementation: sass, webpackImporter: false, sassOptions: { includePaths: [nodeModulePath], }, }, }, ], });   ˞8FCQBDLʹ͍ͭͯਂງΓ͍ͨਓ͸ެࣜυΩϡϝϯτΛࢀরXFCQBDLKTPSH XFCQBDLDPO fi HETBTTKT 8FCQBDLΛར༻͢Δ࠷େͷར఺͸ 4BTT΍-FTT౳ͷ$44ͷϝλݴޠΛ௚઀ར༻Ͱ͖Δ͜ͱ💪 ֎෦Ϟδϡʔϧ͔ΒඞཁͳελΠϧఆٛͷΈΠϯϙʔτ͠ ίϯύΠϧޙͷ$44ͷαΠζΛ࡟ݮ͢Δ͜ͱ΋Ͱ͖Δ💪
  32. $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ 👨🎓ࣄྫ঺հ$0-03.!45&3  ΞΠυϧϚελʔʹొ৔͢ΔΞΠυϧΛݕࡧ ΠϝʔδΧϥʔϓϨϏϡʔ  ͍ͭͲ͜Ͱ΋εϚϗΛϖϯϥΠτԽʂ😆  ϦϦʔε౰ॳ͸,PUMJO .6*

    3FBDU Ͱ࣮૷ ೥݄ʹ$PNQPTFGPS8FCʹϑϧϦϓϨΠε🕺 63-JNBTDPMPSNBTUFSXFCBQQ (JU)VCTVCSPIDPMPSNBTUFS ˞ॳճϦϦʔε͸೥݄
  33. $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ 👨🎓$0-03.!45&3Ͱ࣮ݱͨ͜͠ͱ ػೳ໘   ը໘ભҠͷεϜʔζͳ41"  ͔ͳΓਅ໘໨ʹ ϨεϙϯγϒରԠ 

    μʔΫςʔϚɾଟݴޠ +"&/ ରԠ  (PPHMFΞΧ΢ϯτͱͷ࿈ܞ 
 ਪ͠ͷΞΠυϧͷొ࿥ɾӾཡΛ࣮ݱ  μʔΫςʔϚ μʔΫςʔϚ ӳޠදࣔ ࢒ΓͰ͖͍ͯͳ͍ͷ͸ɺ443ͷಋೖʹΑΔ ॳظඳըͷߴ଎Խɾ4&0ڧԽ͘Β͍ʜʁ🧐
  34. $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ ɹ$0-03.!45&3Ͱར༻͍ͯ͠ΔϥΠϒϥϦ  ,UPS)UUQΫϥΠΞϯτ  LPUMJOYTFSJBMJ[FS+40/γϦΞϥΠβσγϦΞϥΠβ  LPUMJOYDPSPVUJOFTඇಉظॲཧ  LPJO%FQFOEFODZ*OKFDUJPOϥΠϒϥϦ

     +FUQBDL$PNQPTF $PNQPTFGPS8FC 6*ϑϨʔϜϫʔΫ  %FDPNQPTFϧʔςΟϯά ۀ຿ϩδοΫίϯϙʔωϯτͷ੾Γ෼͚  ,PUFTUςετϑϨʔϜϫʔΫ  ˞$0-03.!45&3Ͱ͸ϧʔςΟϯάػೳͷΈ࢖༻ શͯͷϥΠϒϥϦ͕.11ʹެࣜରԠ😎
  35. ɹ$0-03.!45&3Ͱར༻͍ͯ͠ΔϥΠϒϥϦ  NBUFSJBMDPNQPOFOUTXFC.BUFSJBM%FTJHO४ڌͷ6*ϑϨʔϜϫʔΫ  JOFYUݴޠϦιʔε੾Γସ͑  'JSFCBTF+44%,(PPHMFΞΧ΢ϯτ࿈ܞ 'JSFTUPSFΫϥΠΞϯτ  XFCQBDLDEOQMVHJOIUNMXFCQBDLQMVHJOόϯυϧαΠζ࡟ݮ

    $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ  +BWB4DSJQU੡ϥΠϒϥϦ΁ͷґଘ͸جຊ͜Ε͚ͩ .11ରԠͷϥΠϒϥϦΛϑϧ׆༻͢Ε͹ɺ+BWB4DSJQUͷࢿ࢈ʹཔΒͣͱ΋ ͦΕͳΓͷن໛ͷ8FCΞϓϦ͸࣮૷Ͱ͖Δ👍
  36. 👨🎓$0-03.!45&3ͷ࣮૷ղઆ  :sharedϞδϡʔϧҎԼͷσΟϨΫτϦߏ੒ 🗂EBUB  🗂BQJΠϯϑϥ૚ˠ֎෦αʔϏεͱͷ઀ଓ෦෼ͷ࣮૷  📦BVUIFOUJDBUJPOˠ(PPHMFϩάΠϯͷ࣮૷  📦

    fi SFTUPSFˠ'JSFTUPSFΫϥΠΞϯτ  📦JNBTQBSRMˠJN!TQBSRMΫϥΠΞϯτ  📦KT fi SFCBTFBQQˠ'JSFCBTF+44%,ͷܕఆٛ  📦NPEFM  📦SFQPTJUPSZ ΞΠϚεͷ༷ʑͳ৘ใΛΫΤϦݴޠɾ41"32-Ͱ ݕࡧͰ͖ΔϑΝϯϝΠυͷ֎෦αʔϏε )1TQBSRMDSTTOLZYZ[JNBT (JUIVCJNBTJNBTQBSRM $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ  ϓϥοτϑΥʔϜຖͷࠩҟΛٵऩ͠ɺ υϝΠϯɾΞϓϦέʔγϣϯ૚ͱ ֎෦αʔϏεΛૄ݁߹ͳঢ়ଶʹอͭ ˞ ˞
  37. 👨🎓$0-03.!45&3ͷ࣮૷ղઆ  :sharedϞδϡʔϧҎԼͷσΟϨΫτϦߏ੒ 🗂EBUB  🗂BQJ  📦BVUIFOUJDBUJPO  📦

    fi SFTUPSF  📦JNBTQBSRM  📦KT fi SFCBTFBQQ  📦NPEFMυϝΠϯ૚ˠۀ຿ϩδοΫදݱ༻ͷΫϥεఆٛ  📦SFQPTJUPSZυϝΠϯ૚ˠ$36%ૢ࡞ͷఆٛɾந৅Խ $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ  ϓϥοτϑΥʔϜʹґଘ͠ͳ͍ EBUBDMBTTɾϩδοΫͷఆٛ Ұ෦ྫ֎Λআ͖ɺcommonMainʹͷΈ ίʔυ͕ଘࡏ͢Δঢ়ଶͱͳΔ
  38. 👨🎓$0-03.!45&3ͷ࣮૷ղઆ  :sharedϞδϡʔϧҎԼͷσΟϨΫτϦߏ੒ 🗂EBUB  🗂BQJ  📦BVUIFOUJDBUJPO  📦

    fi SFTUPSF  📦JNBTQBSRM  📦KT fi SFCBTFBQQ  📦NPEFM  📦SFQPTJUPSZ $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ  💡,PUMJO.11ͷίϯηϓτͰ͋Δ ۀ຿ϩδοΫͷڞ௨ԽΛ ࣮ݱ͍ͯ͠ΔྖҬ
  39. 👨🎓$0-03.!45&3ͷ࣮૷ղઆ  :sharedϞδϡʔϧҎԼͷσΟϨΫτϦߏ੒ 🗂DPNQPOFOUT  📦DPSF 🗂GFBUVSFT  📦IPNF 

    📦NZJEPMT  📦QSFWJFX  📦TFBSDI $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ  ͦͷଞʹ΋ڞ௨Խίʔυ͕ଘࡏ🤔
  40. 👨🎓$0-03.!45&3ͷ࣮૷ղઆ  :sharedϞδϡʔϧҎԼͷσΟϨΫτϦߏ੒ 🗂DPNQPOFOUTओʹ%*पΓͷίʔυఆٛ  📦DPSF 🗂GFBUVSFT  📦IPNF 

    📦NZJEPMT  📦QSFWJFX  📦TFBSDI $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ  .11ରԠͷ%*ϥΠϒϥϦɾ,PJOʹΑΔ Πϯϑϥ૚ɾυϝΠϯ૚ͷΫϥεͷ ґଘੑ஫ೖॲཧΛهड़ ΞϓϦέʔγϣϯ૚͔Β υϝΠϯ૚ͷίʔυͷΈࢀরՄೳͳ ঢ়ଶΛอͭͨΊʹ༻ҙ👍 LPJO*OTFSU,PJO*0LPJO
  41. 👨🎓$0-03.!45&3ͷ࣮૷ղઆ  :sharedϞδϡʔϧҎԼͷσΟϨΫτϦߏ੒ $PNQPTFGPS8FCͰෳࡶͳΞϓϦΛ࡞Δ  🗂DPNQPOFOUT  📦DPSF 🗂GFBUVSFTϓϨθϯςʔγϣϯ૚ 

    📦IPNF  📦NZJEPMT  📦QSFWJFX  📦TFBSDI ΞϓϦέʔγϣϯ͕ఏڙ͢Δػೳຖʹ 6*ϩδοΫΛఆٛɾ࣮૷ $PNQPTF.VMUJQMBUGPSNΛར༻͠ 6*ϩδοΫ·Ͱڞ௨Խ💪
  42.  🛠ڞ௨Խ͞Εͨ6*ϩδοΫͷத਎ @Composable fun rememberSearchIdolsUseCase( params: SearchParams, language: Languages =

    CurrentLocalLanguage(), koinApp: KoinApplication = CurrentLocalKoinApp(), ): State<LoadState> { val scope = rememberCoroutineScope() val repository: IdolColorsRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) } return produceState<LoadState>( initialValue = LoadState.Initialize, params, ) { val job = scope.launch(start = CoroutineStart.LAZY) { runCatching { repository.search(params, language) } .onSuccess { value = LoadState.Loaded(it) } .onFailure { value = LoadState.Error(it) } } value = LoadState.Loading job.start() } } 4FBSDI*EPMT6TF$BTFLU ྫ໊લ͔ΒΞΠυϧΛݕࡧ͢ΔϩδοΫ ˞commonMain಺Ͱͷ࣮૷ίʔυ
  43. 🛠ڞ௨Խ͞Εͨ6*ϩδοΫͷத਎ @Composable fun rememberSearchIdolsUseCase( params: SearchParams, language: Languages = CurrentLocalLanguage(),

    koinApp: KoinApplication = CurrentLocalKoinApp(), ): State<LoadState> { val scope = rememberCoroutineScope() val repository: IdolColorsRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) } return produceState<LoadState>( initialValue = LoadState.Initialize, params, ) { val job = scope.launch(start = CoroutineStart.LAZY) { runCatching { repository.search(params, language) } .onSuccess { value = LoadState.Loaded(it) } .onFailure { value = LoadState.Error(it) } } value = LoadState.Loading job.start() } }  4FBSDI*EPMT6TF$BTFLU paramsݕࡧΫΤϦ จࣈྻΛอ࣋͢ΔEBUBDMBTT  languagesݱࡏͷݴޠઃఆ koinAppLPJOͷίϯςφΠϯελϯε
  44. 🛠ڞ௨Խ͞Εͨ6*ϩδοΫͷத਎ @Composable fun rememberSearchIdolsUseCase( params: SearchParams, language: Languages = CurrentLocalLanguage(),

    koinApp: KoinApplication = CurrentLocalKoinApp(), ): State<LoadState> { val scope = rememberCoroutineScope() val repository: IdolColorsRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) } return produceState<LoadState>( initialValue = LoadState.Initialize, params, ) { val job = scope.launch(start = CoroutineStart.LAZY) { runCatching { repository.search(params, language) } .onSuccess { value = LoadState.Loaded(it) } .onFailure { value = LoadState.Error(it) } } value = LoadState.Loading job.start() } }  4FBSDI*EPMT6TF$BTFLU LPJOͷίϯςφΠϯελϯε͔Β 3FQPTJUPSZͷΠϯελϯεΛऔಘ
  45. 🛠ڞ௨Խ͞Εͨ6*ϩδοΫͷத਎ @Composable fun rememberSearchIdolsUseCase( params: SearchParams, language: Languages = CurrentLocalLanguage(),

    koinApp: KoinApplication = CurrentLocalKoinApp(), ): State<LoadState> { val scope = rememberCoroutineScope() val repository: IdolColorsRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) } return produceState<LoadState>( initialValue = LoadState.Initialize, params, ) { val job = scope.launch(start = CoroutineStart.LAZY) { runCatching { repository.search(params, language) } .onSuccess { value = LoadState.Loaded(it) } .onFailure { value = LoadState.Error(it) } } value = LoadState.Loading job.start() } }  4FBSDI*EPMT6TF$BTFLU produceStateϝιου ˠҾ਺params͕มԽͨ࣌͠ʹϒϩοΫ಺ͷTVTQFOEؔ਺Λ࣮ߦ͠ ঢ়ଶΛࣗಈߋ৽͢Δ4UBUFܕͷΠϯελϯεΛฦ͢ ˞4UBUFͱ͸ʁ +FUQBDL$PNQPTF͕ఏڙ͢Δ ঢ়ଶ؅ཧ༻ͷΠϯλʔϑΣʔε SFGEFWFMPQFSBOESPJEDPNSFGFSFODFLPUMJOBOESPJEYDPNQPTFSVOUJNF4UBUF
  46. 🛠ڞ௨Խ͞Εͨ6*ϩδοΫͷத਎ @Composable fun rememberSearchIdolsUseCase( params: SearchParams, language: Languages = CurrentLocalLanguage(),

    koinApp: KoinApplication = CurrentLocalKoinApp(), ): State<LoadState> { val scope = rememberCoroutineScope() val repository: IdolColorsRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) } return produceState<LoadState>( initialValue = LoadState.Initialize, params, ) { val job = scope.launch(start = CoroutineStart.LAZY) { runCatching { repository.search(params, language) } .onSuccess { value = LoadState.Loaded(it) } .onFailure { value = LoadState.Error(it) } } value = LoadState.Loading job.start() } }  4FBSDI*EPMT6TF$BTFLU ݕࡧΫΤϦͷQPTU ݁ՌͷऔಘΛ࣮ߦ͢ΔTVTQFOEͳؔ਺ ݕࡧΫΤϦ͕มԽ͢Δ౓ʹɺ͜ͷؔ਺͕ࣗಈ࣮ߦ͞ΕΔ -PBE4UBUFΫϥεˠඇಉظॲཧͷঢ়ଶΛද͢ sealed class LoadState { object Initialize : LoadState() object Loading : LoadState() data class Loaded<out T>(val value: T) : LoadState() data class Error(val error: Throwable) : LoadState() }
  47. 🛠ڞ௨Խ͞Εͨ6*ϩδοΫͷݺͼग़͠ํ  @Composable fun FrontLayerContent( params: SearchParams, coroutineScope: CoroutineScope, backdropScaffoldState:

    BackdropScaffoldState, snackbarHostState: SnackbarHostState, launchPreviewScreen: (ScreenType, List<String>) -> Unit, ) { val idolColorLoadState by rememberSearchIdolsUseCase(params) val items = idolColorLoadState.getValueOrNull() ?: listOf<IdolColor>() Column { SearchStateLabel( params, idolColorLoadState, // 検索結果の表示UIの描画 // ...略 ) } } @Composable fun FrontLayer( backdropState: MutableState<BackdropValues>, isSignedIn: Boolean, params: SearchParams, ) { // ...略 val idolColorLoadState by rememberSearchIdolsUseCase(params) SearchResultList( isSignedIn, idolColorLoadState, // 検索結果の表示UIの描画 // ...略 ) } 'SPOU-BZFSLU 'SPOU-BZFS$POUFOULU
  48. 🛠ڞ௨Խ͞Εͨ6*ϩδοΫͷݺͼग़͠ํ  @Composable fun FrontLayerContent( params: SearchParams, coroutineScope: CoroutineScope, backdropScaffoldState:

    BackdropScaffoldState, snackbarHostState: SnackbarHostState, launchPreviewScreen: (ScreenType, List<String>) -> Unit, ) { val idolColorLoadState by rememberSearchIdolsUseCase(params) val items = idolColorLoadState.getValueOrNull() ?: listOf<IdolColor>() Column { SearchStateLabel( params, idolColorLoadState, // 検索結果の表示UIの描画 // ...略 ) } } @Composable fun FrontLayer( backdropState: MutableState<BackdropValues>, isSignedIn: Boolean, params: SearchParams, ) { // ...略 val idolColorLoadState by rememberSearchIdolsUseCase(params) SearchResultList( isSignedIn, idolColorLoadState, // 検索結果の表示UIの描画 // ...略 ) } 'SPOU-BZFSLU 'SPOU-BZFS$POUFOULU ݕࡧ݁Ռͷऔಘ 8FC ݕࡧ݁Ռͷऔಘ "OESPJE 💡ϒϦοδͷίʔυΛ༻ҙ͢Δ͜ͱͳ͘ ࠷খݶͷهड़Ͱ6*ϩδοΫ ঢ়ଶ؅ཧʹΞΫηε😎 ˠϓϥοτϑΥʔϜݻ༗ͷ෦෼΋ಉ͡ϑϨʔϜϫʔΫΛར༻ ϒϦοδͷͭΒΈΛղফ🎉ݻ༗෦෼ͷ࣮૷ʹΑΓਂ͘஫ྗ🎉
  49. 🛠ڞ௨Խ͞Εͨ6*ϩδοΫͷݺͼग़͠ํ  @Composable fun FrontLayerContent( params: SearchParams, coroutineScope: CoroutineScope, backdropScaffoldState:

    BackdropScaffoldState, snackbarHostState: SnackbarHostState, launchPreviewScreen: (ScreenType, List<String>) -> Unit, ) { val idolColorLoadState by rememberSearchIdolsUseCase(params) val items = idolColorLoadState.getValueOrNull() ?: listOf<IdolColor>() Column { SearchStateLabel( params, idolColorLoadState, // 検索結果の表示UIの描画 // ...略 ) } } @Composable fun FrontLayer( backdropState: MutableState<BackdropValues>, isSignedIn: Boolean, params: SearchParams, ) { // ...略 val idolColorLoadState by rememberSearchIdolsUseCase(params) SearchResultList( isSignedIn, idolColorLoadState, // 検索結果の表示UIの描画 // ...略 ) } 'SPOU-BZFSLU 'SPOU-BZFS$POUFOULU ݕࡧ݁Ռͷऔಘ 8FC ݕࡧ݁Ռͷऔಘ "OESPJE 💡ϒϦοδͷίʔυΛ༻ҙ͢Δ͜ͱͳ͘ ࠷খݶͷهड़Ͱ6*ϩδοΫ ঢ়ଶ؅ཧʹΞΫηε😎 ˠϓϥοτϑΥʔϜݻ༗ͷ෦෼΋ಉ͡ϑϨʔϜϫʔΫΛར༻ ϒϦοδͷͭΒΈΛղফ🎉ݻ༗෦෼ͷ࣮૷ʹΑΓਂ͘஫ྗ🎉 ,PUMJO΍ͬͺΓఱ࠽͔ʜʁ🥰 ΋͏ੈքͷશͯΛ,PUMJOͰॻ͚͹ ͑͑ͷͰ͸ʜʁ🤔 ओޠσΧൃݴ
  50. ,PUMJO+4Ͱͷ8FCϑϩϯτ։ൃ୹ॴ 😢ݟͨ໨෦෼ͷ࣮૷ʹ$44ͷ஌ࣝΛཁٻ͞ΕΔ  $PNQPTFGPS8FCͰ͸.PEJ fi FS͸ݪଇ࢖༻ෆՄ˞QSFWJFXͷػೳͰ͸ར༻Մೳɺޙड़ 😢+BWB4DSJQU੡Ϟδϡʔϧͷར༻ʹͻͱख͔͔ؒΔ  +BWB4DSJQUͷΦϒδΣΫτɾϝιουͷར༻ʹ͸,PUMJOͰͷܕఆ͕ٛඞཁ 

    .11ରԠͷ,PUMJO੡ϥΠϒϥϦͳΒ˕ɺ͔͠͠ࢿ࢈ͷྔ͸ 😢ෳࡶͳ͜ͱΛ͠Α͏ͱͨ࣌͠ɺ8FCQBDLͷઃఆΛ͍͡Δඞཁ͕͋Δ  ϏϧυγεςϜͷ஌ࣝΛ਎ʹண͚Δͷ͸ɺجຊͲͷྖҬͰ΋ϋʔυ  "OESPJEΤϯδχΞʹͱͬͯ ࠷େͷࢀೖোน😖
  51. ,PUMJO+4ͷݱࡏ ೥ؒͷ,PUMJO+4ͷਐԽ ൈਮ  ,PUMJO🚀  ,PUMJO+4OFX*3DPNQJMFSЋ൛ϦϦʔεɻόϯυϧαΠζͷେ෯ͳ࡟ݮ 5ZQF4DSJQUͷܕఆٛϑΝΠϧੜ੒ɻ  (SBEMF%4-΁ͷػೳ௥Ճɻbinaries.executable()cssSupport(dev|optional|peer)Npm౳ɻ

    ,PUMJO🚀  (SBEMF%4-΁ͷػೳ௥Ճɻpackage.jsonΛ(SBEMFεΫϦϓτ಺ͰΧελϚΠζͰ͖ΔΑ͏ʹɻ ,PUMJO🚀  ϑϨʔϜϫʔΫɾϥΠϒϥϦ޲͚ͷϏϧυͰOFX*3DPNQJMFS͕ར༻Մೳʹɻ ,PUMJO🚀  ,PUJO+4OFX*3DPNQJMFSЌ൛ϦϦʔεɻ8FCϑϩϯτ։ൃʹ͓͚Δσόοάػೳͷվળɻ ,PUMJO🚀  importจʹΑΔ+BWB4DSJQUϞδϡʔϧͷಈతಡΈࠐΈʹରԠɻOFX*3DPNQJMFSͷύϑΥʔϚϯεվળɻ  $PNQPTFGPS8FCςΫχΧϧϓϨϏϡʔ൛ϦϦʔε $PNQPTF.VMUJQMBUGPSNЋ൛ˠЌ൛ˠ4UBCMF൛ϦϦʔε $PNQPTF.VMUJQMBUGPSNϦϦʔε (dev|optional|peer)Npm͸ ͕௥Ճ😎 ,PUMJOຊମͷϦϦʔεͷ౓ʹ ຖճԿ͔͠Βͷػೳ௥Ճɾվળ͕͞Ε͍ͯΔ✨
  52. ,PUMJO+4ͷࠓޙͷల๬ 💫OFX*3DPNQJMFSͷ4UBCMF൛ϦϦʔε  όϯυϧαΠζ࡟ݮɺ+4ίʔυ΁ͷτϥϯεύΠϧߴ଎Խʹେ͖͘ߩݙ  ΠϯΫϦϝϯλϧίϯύΠϧ +4ͱͷ૬ޓӡ༻ੑʹ஫ྗ͠ɺ҆ఆԽ  ͜ͷλεΫ͕׬ྃ͢Δͱɺ&4ରԠ͕࠶։͞ΕΔ ͸ͣ

     💫$PNQPTFGPS8FCͷ4LJBରԠ  $PNQPTFGPS8FCͰ΋.PEJ fi FSΛ࢖ͬͨ 
 6*࣮૷͕Մೳʹ  ˞,PUMJO࣌఺Ͱ͸Ќ൛ ˞$PNQPTF.VMUJQMBUGPSN࣌఺Ͱ͸1SFWJFX൛ ཁνΣοΫʂ 😉 Ϟμϯ+4ͱͷ૬ޓӡ༻ੑ͕ େ͖͘ߴ·Δ ͸ͣ