LoginSignup
58
36

More than 3 years have passed since last update.

Next.js + Emotion (CSS in JS)で始めるReact超入門

Last updated at Posted at 2019-12-21

以前、React初学者向けの勉強会を開催したときに作った資料を、Qiita向けに調整したものです。

  • Reactの初歩的な記法
  • Next.jsでのサーバーサイドレンダリングの概要
  • CSSinJSの使い方

を学ぶことができます。

事前にNode.jsをインストールしている必要があります。

Reactとは?

Reactとは、Facebookが作ったJavaScriptライブラリです。ユーザーインターフェイスをコンポーネントベースで作ることができます。

公式サイト:https://ja.reactjs.org/

シンプルなReactのサンプル

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World</title>
    <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  </head>

  <body>
    <div id="root"></div>

    <!-- babelがJSXをReact.createElement()に変換してくれる -->
    <script type="text/babel">
      /**
       * 引数:propsを、JSX:<div>{props.text}</div>で受け取り、returnで返す。
       * この一式をコンポーネントと呼ぶ。
       */
      function Hello(props) {
        return <div>{props.text}</div>
      };

      /**
       * Helloコンポーネントを<div id="root"></div>にマウントしている。
       * 関数みたいにしてみると、`Hello({ text: 'Hello, React!' });` こんな感じ。
       */
      ReactDOM.render(
        <Hello text="Hello, React!" />,
        document.getElementById("root")
      );
    </script>
  </body>
</html>

Reactの特徴

Vue.jsがディレクティブを使いHTMLを拡張するような方法で開発するのに対し、ReactはガシガシJavaScriptを書いていきます。まあ、Vue.jsもガッツリ開発を始めるとガシガシJavaScriptを書くことになると思いますが(・ω・)

また、Reactは基本的にデータを受け取って適切なViewを返すことを目的としたシンプルなライブラリなので、Angularのようなルールはなく、自由度がかなり高いです。逆に言うと、しっかりとした設計ができてないと、開発途中でつらくなります(・ω・)

宣言的UI

Reactに限らず、近年のUIフレームワーク・ライブラリ、およびプログラミングにおいて主要なパラダイムである宣言的UIについて、知っておく必要があります。

そのまえに、まず、命令的と宣言的を解説します。

命令的

  • 何をするかを記述する
  • 前回の実行結果に依存する
    • 変数の再代入が行われる

宣言的

  • どういう状態になるのかを記述する
  • 前回の実行結果に依存しない
    • 変数に再代入しない

命令的UIと宣言的UI

命令的UI

命令的UIの例として、jQueryによるDOM操作があげられます。

<!-- この時点ではUIの最終的な状態はわからない -->
<ul id="list"></ul>
const animals = ["ねずみ", "うし", "とら"];

// 配列の要素分処理を繰り返し、HTML側に挿入することでUIが決定する。
animals.forEach(animal => {
  $('#list').append(`<li>${animal}</li>`);
});

宣言的UI

一方で、Vue.jsは宣言的にUIを作ることができます。

<template>
  <ul>
    <!-- この時点でUIの状態が決まっている -->
    <li v-for="(item, index) in list" :key="index">{{item}}</li>
  </ul>
</template>

<script>
  export default {
    data() {
      return {
        // 配列の要素によってリストの数が決定する
        list: ["ねずみ", "うし", "とら"]
      };
    }
  };
</script>

ReactやVue.jsは、jQueryの次に流行っているフレームワーク・ライブラリではなく、宣言的なUIを作るためのフレームワーク・ライブラリです。

技術選定時に宣言的なUIが必要であれば、ReactやVue.jsを使用しましょう。逆に言えば、jQueryのほうが適切な場面であれば、無理に使用する必要はありません。

Next.jsとは?

Next.jsとは、Reactでサーバーサイドレンダリングをするためのフレームワークです。Vue.jsで言うところの、Nuxt.js。簡単にルーティングできて、静的サイトの書き出しもできます。

公式サイト:https://nextjs.org/

静的サイトの書き出しならば、Gatsby.jsのほうが使いやすいかもしれませんが、Next.jsのほうがシンプルに始められるので、今回はNext.jsを使用します。

Reactとしての書き方はほぼ同じですし、メジャーなエコシステムも使用できるなので、Next.jsが使えればGatsby.jsも使えると思います。多分。

サーバーサイドレンダリングとは?

サーバーサイドレンダリングとは、PHPRubyJavaのように、サーバーサイドでDOMを生成してクライアントに静的なHTMLとして渡すことです。

Reactは、本来クライアントサイドで仮想DOMを生成し、それを実DOMとしてブラウザに描画します。

<!-- サーバーから返されるHTML -->
<div id="app"></div>
import React from 'react';
import ReactDOM from "react-dom";

// クライアントサイドでDOMを書き換える
ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

サーバーサイドレンダリングは、サーバー上でReactを実行し、生成したDOMをクライアントへ渡します。

たとえば、動的なコンテンツで<title>要素や<meta>要素をクライアントサイドで生成すると、TwitterやFacebook等のSNSでシェアしたときには反映されません。

しかし、サーバーサイド上で事前にDOMを生成すれば、クライアントからみれば静的なHTMLがレスポンスとして返ってくるので、この問題が回避できます。

Next.jsの使い方

必要なパッケージをインストール

  • next
  • react
  • react-dom
$ mkdir nextjs-sample
$ cd nextjs-sample
$ npm init -y
$ npm i next react react-dom

package.jsonscriptsを追記。

{
  "scripts": {
    "dev": "next"
  }
}

./pagesディレクトリにindex.jsを追加。

$ mkdir pages
$ touch pages/index.js
// pages/index.js
export default () => <h1>Hello, Next.js!</h1>

ローカルサーバーを起動。

$ npm run dev

http://localhost:3000にアクセスして、Hello, Next.js!が表示されていれば、OK!

ディレクトリ構成

  • ./pages ルーティングの対象
  • ./static 静的ファイルの置き場所
    • 画像ファイルとか

ディレクトリのルールが決まっているのは、これくらい。

また、Next.js 9.1からsrc配下でも利用できるようになったので、以下でもOKです。

  • src/pages ルーティングの対象
  • src/static 静的ファイルの置き場所

JSX

JSXを使用すると、JavaScript上でHTMLのような構文が使えます。

// JSX
const Button = <button className="my-button">ボタン</button>

これは、React.createElement()の糖衣構文で、JSXを使わないと下記の記述になります。

const Button = React.createElement("button", {
  className: "my-button"
}, "ボタン");

極論、JSXを使わずにReact.createElement()を使ってもなんの問題ありません。

JSXを使う理由が公式のガイドにありますので、興味のある方はどうぞ。

オンライン Babel コンパイラを使うと、JSXがどのようなJavaScriptに変換されるのかを確認できます。

Next.js(React)を書いてみよう

pages/index.jsを、省略形なしの形に変更。

import React from 'react' // Next.jsでは省略可能

// returnでJSXを返す関数をコンポーネントと呼ぶ
function Index() {
  return <h1>Hello, Next.js!</h1>
}

// ES Modules
// 本来は import されて react-dom がレンダリングするが、Next.jsでは隠蔽されている
export default Index

HTMLのように、JSXでも子要素を使うことができます。

import React from 'react'

function Index() {
  // ()で括り、;の自動挿入に対応
  // returnで返すJSXは必ず1つの要素
  return (
    <div>
      <h1>Hello, Next.js!</h1>
    </div>
  )
}

export default Index

JSXは{}でJavaScriptを使うことができます。

import React from 'react'

function Index() {
  const text = 'Next.js!'
  return (
    <div>
      {/* コメントアウト */}
      <h1>{`Hello, ${text}`}</h1>
    </div>
  )
}

export default Index

Headingコンポーネントを作って、JSX内で使ってみましょう。

import React from 'react'

// 見出し用のコンポーネント
function Heading(props) {
  // 属性の値は、オブジェクトのプロパティとして渡される
  return <h1>{props.text}</h1>
}

function Index() {
  const text = 'Next.js!'
  return (
    <div>
      {
        /**
        * コンポーネントの属性でテキストを渡す
        * これをProps(プロップス)と呼ぶ
        */
      }
      <Heading text={`Hello, ${text}`} />
    </div>
  )
}

export default Index

Headingコンポーネントに、子要素を渡してみます。

import React from 'react'

// Propsはオブジェクトなので、分割代入が使える
function Heading({ children }) {
  // childrenで子要素を受け取る
  return <h1>{children}</h1>
}

function Index() {
  const text = 'Next.js!'

  return (
    <div>
      {/* コンポーネントの子要素でspan要素を渡す */}
      <Heading>
        <span>{`Hello, ${text}`}</span>
      </Heading>
    </div>
  )
}

export default Index

divがいらねえときは、React.Fragmentが便利です。

import React from 'react'

function Heading({ children }) {
  return <h1>{children}</h1>
}

function Index() {
  const text = 'Next.js!'
  // React.Fragmentを使うとその要素はレンダリングされない
  return (
    <React.Fragment>
      <Heading>
        <span>{`Hello, ${text}`}</span>
      </Heading>
      <p>divでラップしたないねん</p>
    </React.Fragment>
  )
}

export default Index

React.Fragmentは、糖衣構文として<></>とも使えます。記述量が遥かに少なくてすむので、とくに理由がなければ、こちらを使用しましょう。

import React from 'react'

function Heading({ children }) {
  return <h1>{children}</h1>
}

function Index() {
  const text = 'Next.js!'
  // <React.Fragment></React.Fragment>は<></>とも書ける
  return (
    <>
      <Heading>
        <span>{`Hello, ${text}`}</span>
      </Heading>
      <p>divでラップしたないねん</p>
    </>
  )
}

export default Index

ファイルを分けてみましょう。

$ mkdir components 
$ touch components/Heading.js
// components/Heading.js
// {} と return を省略できる
function Heading({ children }) {
  return <h1>{children}</h1>
}

export default Heading
// pages/index.js
import React from 'react'
import Heading from '../components/Heading'

function Index() {
  const text = 'Next.js!'
  return (
    <>
      <Heading>
        <span>{`Hello, ${text}`}</span>
      </Heading>
      <p>divでラップしたないねん</p>
    </>
  )
}

export default Index

mapメソッドで要素の反復処理をしてみましょう。

// pages/index.js
import React from 'react'
import Heading from '../components/Heading'

// 配列
const member = ['ネズミ', '', 'トラ', 'うさぎ']

function Index() {
  const text = 'Next.js!'
  return (
    <>
      <Heading>
        <span>{`Hello, ${text}`}</span>
      </Heading>
      <ul>
        {member.map((name, index) => (
          <li key={index}>{name}</li>
        ))}
      </ul>
    </>
  )
}

export default Index

onClickでイベント発火できます。

// pages/index.js
import React from 'react'
import Heading from '../components/Heading'

const member = ['ネズミ', '', 'トラ', 'うさぎ']

function Index() {
  const text = 'Next.js!'
  return (
    <>
      <Heading>
        <span>{`Hello, ${text}`}</span>
      </Heading>
      <ul>
        {member.map((name, index) => (
          <li key={index}>{name}</li>
        ))}
      </ul>

      {/* onClickに関数を書く */}
      <button onClick={() => console.log('onClick')}>ボタン</button>
    </>
  )
}

export default Index

useStateで関数コンポーネントに状態をもたせる

React 16.8で、hooksという新機能が追加されました。Reactでstateなどの機能を使う場合、これまではクラスで書かないといけませんでしたが、hooksの登場で関数コンポーネントでも副作用のある機能を使うことができるようになりました。

今回は、関数コンポーネントに状態をもたせることができる、useStateを使ってみましょう。

// pages/index.js
// `useState`をインポート
import React, { useState } from 'react'
import Heading from '../components/Heading'

const member = ['ネズミ', '', 'トラ', 'うさぎ']

function Index() {
  const text = 'Next.js!'

  /**
 * const [変数, 変数の値を変える関数] = useState(初期値)
 * 以下では、`value`変数の初期値に`No, Click.`の文字列を代入しています。
 * setValue('Yes, Click!!')を実行すると、
 * valueの値を`No, Click.`から`Yes, Click!!`に変えることができます。
 */
  const [value, setValue] = useState('No, Click.');
  const onClickEvent = () => setValue('Yes, Click!!');

  return (
    <>
      <Heading>
        <span>{`Hello, ${text}`}</span>
      </Heading>
      <ul>
        {member.map((name, index) => (
          <li key={index}>{name}</li>
        ))}
      </ul>
      <button onClick={() => console.log('onClick')}>ボタン</button>

      {/* クリックすると、`No, Click.`が`Yes, Click!!`に変わる */}
      <button onClick={onClickEvent}>{value}</button>
    </>
  )
}

export default Index

Next.js独自の機能

Linkコンポーネントでルーティングさせてみましょう。

// pages/index.js
import React, { useState } from 'react'
import Link from 'next/link' // Linkコンポーネントを追加
import Heading from '../components/Heading'

const member = ['ネズミ', '', 'トラ', 'うさぎ']

function Index() {
  const text = 'Next.js!'
  const [value, setValue] = useState('No, Click.');
  const onClickEvent = () => setValue('Yes, Click!!');
  return (
    <>
      <Heading>
        <span>{`Hello, ${text}`}</span>
      </Heading>
      <ul>
        {member.map((name, index) => (
          <li key={index}>{name}</li>
        ))}
      </ul>
      <button onClick={() => console.log('onClick')}>ボタン</button>
      <button onClick={onClickEvent}>{value}</button>

      {/* Linkコンポーネントでルーティングできる */}
      <Link href="/batman"><a>バットマンページへ</a></Link>
    </>
  )
}

export default Index

pages/batman.jsを作成した上、バットマンページへのリンクをクリックすると、再読み込みなしでページ遷移できます。つまり、SPAです。

$ touch pages/batman.js
// pages/batman.js
import React from 'react'

function Batman() {
  return <div>batman</div>
}

export default Batman

getInitialPropsで非同期データ取得

getInitialPropsは、Next.jsのライフサイクルメソッドです。ページが読み込まれたときはサーバーサイドで実行され、以降、Linkコンポーネントによって別のpagesコンポーネントへ移動した場合にクライアントサイドで実行されます。

以下の実装をして、http://localhost:3000からhttp://localhost:3000/batmanに遷移したときと、http://localhost:3000/batmanをリロードしたときのコンソールの表示を確認してみましょう。

// pages/batman.js
import React from 'react'

function Batman({ text }) {
  return <div>{text}</div>
}

Batman.getInitialProps = () => {
  const text = 'I am Batman !!'
  return { text } // returnしたオブジェクトをコンポーネントのPropsとして受け取れます
}

export default Batman

遷移したときはブラウザ側のコンソール、リロードしたときは開発側のコンソールに、それぞれログが出たかと思います。

Next.jsはサーバーサイドレンダリングのためのフレームワークなので、今書いているJavaScriptがサーバーサイド(Node.js)なのか?それとも、クライアントサイドなのか?を意識することが必要です。

非同期でデータ取得

バットマンAPIを叩いて、非同期に情報を取得してみましょう。ページ読み込み時になにかしらの処理をする場合は、getInitialPropsメソッドを使います。

Node.jsではfetchメソッドが使えないので、isomorphic-unfetchをインストールして使います。

$ npm i isomorphic-unfetch
// pages/batman.js
import React from 'react'
import fetch from 'isomorphic-unfetch'

function Batman({ shows }) {
  return (
    <div>
      <h1>Batman TV Shows</h1>
      <ul>
        {shows.map(show => (
          <li key={show.id}>
            <div><img src={show.image.medium} /></div>
            <div>{show.name}</div>
          </li>
        ))}
      </ul>
    </div>
  )
}

Batman.getInitialProps = async () => {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman')
  const data = await res.json();
  return {
    shows: data.map(entry => entry.show)
  }
}

export default Batman

サーバーサイドレンダリングの使い所

たとえば、動的なコンテンツで<title>要素や<meta>要素をクライアントサイドで生成すると、TwitterやFacebook等のSNSでシェアしたときには反映されません。

しかし、サーバーサイド上で事前にDOMを生成すれば、クライアントからみれば静的なHTMLがレスポンスとして返ってくるので、この問題が回避できます。

クソアプリを作ったので、これを実際に試してみましょう。

$ touch pages/nameApp.js
$ touch pages/yourName.js
// nameApp.js
import React, { useState } from 'react'
import { useRouter } from 'next/router'

function NameApp() {
  const [name, setValue] = useState('')

  /**
   * Next.jsのルーターオブジェクト
   * https://nextjs.org/docs#userouter
   */
  const router = useRouter()

  const onClickEvent = () => {
    // yourname?name=【name】に遷移する
    router.push({
      pathname: '/yourName',
      query: { name },
    })
  }

  const onChangeEvent = event => setValue(event.target.value)

  return (
    <>
      <div>君の名は。。。</div>
      <input value={name} onChange={onChangeEvent} />
      <button onClick={onClickEvent}>click!!</button>
    </>
  )
}

export default NameApp

nameApp.jsのやっていることは、ReactやNext.jsを使わない方法で書くとこんな感じです。

<form action="yourName/" method="GET">
  <div>君の名は。。。</div>
  <input name="name"/>
  <button type="submit">click!!</button>
</form>

続いて、遷移先のyourName.jsを実装します。

// yourName
import React from 'react'
import Head from 'next/head'

function YourName({ query }) {
  const { name } = query
  return (
    <>
      {/* Headコンポーネントで`title`や`meta`が設定できる */}
      <Head>
        <title>{name} | YourName</title>
        <meta name="description" content={`君の名は、${name}ですね。`} />
      </Head>
      <div>
        君の名は、<strong>{name}</strong>ですね。
      </div>
    </>
  )
}

YourName.getInitialProps = ({ query }) => {
  return { query }
}

export default YourName

フォームに名前を入力して隣のボタンをクリックすると、入力した名前を表示することができる画期的なアプリです。

command + option + uでソースを確認してみましょう。サーバーから取得したHTMLの段階で、titlemetaが設定されていることがわかります。

イメージを掴んでいただくために、試しにPHPで実装してみました。(PHPが全然わからないので細かいところはご勘弁を。。。(´;ω;`))

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>YourName</title>
</head>
<body>
  <form action="yourName/" method="GET">
    <div>君の名は。。。</div>
    <input name="name"/>
    <button type="submit">click!!</button>
  </form>
</body>
</html>

上のHTMLでyourName/?name=ほげぼげみたいな感じになるので、PHPでパラメーターを受け取りHTMLとしてクライアントにレスポンスします。

<!-- yourName/index.php -->
<?php $name = htmlspecialchars($_GET['name']); ?>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title><?php echo $name; ?> | YourName</title>
  <meta name="description" content="君の名は、<?php echo $name; ?>ですね。" />
</head>
<body>
  <div>君の名は、<?php echo $name; ?>ですね。</div>
</body>
</html>

より掘り下げたい場合は、公式ドキュメントを確認してください。

また、GitHubのexampleに豊富なサンプルがあるので、とても参考になります。

EmotionでCSS in JS

Emotionとは、JavaScriptでCSSスタイルを記述するために設計されたライブラリです。後発ライブラリのため、styled-component等の良いとこ取りをしています。

必要なパッケージをインストールしましょ。

  • @emotion/styled
  • @emotion/core
$ npm i @emotion/styled @emotion/core

Emotionを使ってみよう

@emotion/styledを使い、styled-componentライクなコンポーネントを作ってみます。

// components/Heading.js
// @emotion/styledをインポート
import styled from '@emotion/styled'

// styled.{要素}`{css}` の形で使用します。
// 定数に代入することで、コンポーネントとして利用できます。
// ブラウザ上ではユニークな文字列のCSSクラスが付与されるので、CSSはスコープになります。
const HeadingStyle = styled.h1`
  font-size: 20px;
  color: red;
`

function Heading({ children }) {
  return <HeadingStyle>{children}</HeadingStyle>
}

export default Heading

CSS部分はテンプレートリテラルなので、${}内でJavaScriptが利用できます。

// components/Heading.js
import styled from '@emotion/styled'

// フォントサイズを定数化
const fontSize = 20

// テンプレートリテラル内で定数を使用
const HeadingStyle = styled.h1`
  font-size: ${fontSize}px;
  color: red;
`

function Heading({ children }) {
  return <HeadingStyle>{children}</HeadingStyle>
}

export default Heading

コンポーネント側からProps経由で値を渡すことができます。別ファイルにしてデータを渡してみましょう。

$ touch components/HeadingStyle.js
// components/HeadingStyle.js
import styled from '@emotion/styled'

// ES Modules
// 関数の引数としてデータを受け取ります
export const HeadingStyle = styled.h1`
  font-size: ${props => props.fontSize}px;
  color: red;
`
// components/Heading.js
import { HeadingStyle } from './HeadingStyle'

const fontSize = 20

function Heading({ children }) {
  return <HeadingStyle fontSize={fontSize} >{children}</HeadingStyle>
}

export default Heading

SCSSのようにネストが使えます。

// components/HeadingStyle.js
import styled from '@emotion/styled'

// SCSSのように&が使えます。
export const HeadingStyle = styled.h1`
  font-size: ${props => props.fontSize}px;
  color: red;

  &:hover {
    color: green;
  }
`

CSS in JSのメリット

たとえば、ブレークポイントをJavaScriptで管理すれば、Carouselのライブラリ等と共通の値を使うことができます。

$ mkdir const
$ touch const/breakPoints.js
// const/breakPoints.js
const breakPoints = {
  xs: 0,
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1200
}

export default breakPoints
// components/HeadingStyle.js
import styled from '@emotion/styled'
import breakPoints from '../const/breakPoints'

export const HeadingStyle = styled.h1`
  font-size: ${props => props.fontSize}px;
  color: red;

  @media (min-width: ${breakPoints.md}px) {
    color: green;
  }
`;

react-slickを使ってみましょ。

$ npm i react-slick slick-carousel raw-loader
$ touch next.config.js

next.config.jsで、Next.jsが隠蔽しているwebpackの設定にアクセスできます。raw-loaderを追加して、CSSファイルを扱えるようにします。

// next.config.js
module.exports = {
  webpack: config => {
    config.module.rules.push({
      test: /\.css$/,
      use: "raw-loader"
    });
    return config
  }
}

スライダーコンポーネントを作り、トップページで使ってみましょう。

$ touch components/MySlider.js
// components/MySlider.js
import React from 'react'
import styled from '@emotion/styled'
import Slider from 'react-slick'
import slickCss from 'slick-carousel/slick/slick.css'
import slickThemeCss from 'slick-carousel/slick/slick-theme.css'
import breakPoints from '../const/breakPoints'

const settings = {
  infinite: false,
  slidesToShow: 2,
  slidesToScroll: 2,
  responsive: [
    {
      breakpoint: breakPoints.md, // const/breakPoints.jsの値が使える
      settings: {
        infinite: true,
        slidesToShow: 1,
        slidesToScroll: 1,
      }
    }
  ]
};

const SliderWrapperStyle = styled.div`
  ${slickCss}
  ${slickThemeCss}
`

function MySlider({ member }) {
  return (
    <SliderWrapperStyle>
      <Slider {...settings}>
       {member.map((animal, index) => (
         <div key={index}>{animal}</div>
       ))}
      </Slider>
    </SliderWrapperStyle>
  )
}

export default MySlider
// pages/index.js
import React, { useState } from 'react'
import Link from 'next/link'
import MySlider from "../components/MySlider";
import Heading from '../components/Heading'

const member = ['ネズミ', '', 'トラ', 'うさぎ']

function Index() {
  const text = 'Next.js!'
  const [value, setValue] = useState('No, Click.');
  const onClickEvent = () => setValue('Yes, Click!!');
  return (
    <>
      <Heading>
        <span>{`Hello, ${text}`}</span>
      </Heading>
      <button onClick={() => console.log("onClick")}>ボタン</button>
      <button onClick={onClickEvent}>{value}</button>
      <Link href="/batman">
        <a>バットマンページへ</a>
      </Link>

      {/* member配列をPropsで渡す */}
      <MySlider member={member} />
    </>
  );
}

export default Index

他にもEmotionでいろいろなことができるので、ぜひ掘り下げてみてください。

公式ガイド:https://emotion.sh/docs/introduction

58
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58
36