LoginSignup
8
1

More than 3 years have passed since last update.

【第3回】「みんなのポートフォリオまとめサイト」を作ります~苦悩の連続編~

Posted at

はじめに

この連載記事は、僕が「みんなのポートフォリオまとめサイト」を作る過程をゼロから発信しながらみなさんに見てもらいつつ、作っている途中からみなさんにアドバイスをいただいて、よりよいサービスにしていきたいというお話です。

あとは「サービスを作っていく過程って初学者の人にとっては結構興味ある内容だったりするのでは?(少なくとも自分は知りたかった!)」と思い、このようなスタイルで記事を書いています。

一週間に一度のペースで更新しますとか言っちゃいましたが、前回更新からなんと2ヶ月も経ってしまった!
今後も完成までゆるく更新していく予定です。

前回までの記事

【第0回】「みんなのポートフォリオまとめサイト」を作ります~宣言編~
「とりあえずこれから作るからみんな見てて!」と宣言しただけの記事。

【第1回】「みんなのポートフォリオまとめサイト」を作ります~着手編~
仕様を決めたり、サービス名考えたり、デザインを作ったり。

【第2回】「みんなのポートフォリオまとめサイト」を作ります~SPA認証で死闘編~
いよいよ実装に入っていきます。

今回は、実装に入って途中からTypeScriptやテストを導入してみたりした話です。

これまでやったことと作業時間

集計期間 : 10/7〜12/8(単なる自分用のメモです)

やったこと 前回までの作業時間 ( h ) 今回の作業時間 ( h )
仕様決め(競合調査など) 3.0
サービス名考案 2.0
WF作成 2.0
デザイン作成 8.0
DB設計 4.0
実装(環境構築) 4.5 15.0
実装 25.0 82.0
その他 2.0

作業時間計 ( h )

前回まで 今回 合計
50.5 97.0 147.5

んー 2ヶ月で97hか。

、、、少ない!
もちろんプライベートの全ての時間をこの開発に費やしていた訳ではないけれど、なにをやっていたんだろうか?(前回前々回も同じこと言ってる。)

目標は、2月中にリリース!!!(宣言しちゃう!)
粛々と進めるしかないわよ。

今回やったこと

この2ヶ月でやったことを時系列で列挙していきます〜

デザイナーさん音信不通、、、からの秒速で次のデザイナーさんへ

早速ですが、タイトルロゴのデザインをお願いしていたデザイナーさんが音信不通になってしまいました。。(シンプルに悲しい

以前副業案件でお世話になったデザイナーさんで、クオリティ高いデザインを作ってくださりコミュニケーションも非常に気持ちのいい方だったので、迷わず今回もお願いしたんですケドね。。

まあのっぴきならない事情があったのでしょう。。
いつまでも待ってる訳にはいかないので代わりのデザイナーさんを探すことに。

正直他のデザイナーさんのツテはなかったので、「あーまたココナラで探さなきゃだな。。」とこの後またイチからデザイナーさんを探す労力を想像して落胆しつつ、Twitterで嘆きを投稿しました。
https://twitter.com/kiwatchi1991/status/1314719951777792000?s=20
スクリーンショット 2020-12-08 22.22.24.png

すると、すぐにたくさんのコメントを頂きました。(本当にありがとうございます。やっぱりTwitterはすごい。。

その中の1つに、以前オンラインのワークショップで知り合ったデザイナーさんを紹介してくださるコメントがあり、ソッコーでDMを送らせてもらってその日のうちに代わりのデザイナーさんを見つけることができました( 柳田さん(@asami_lin) 本当にありがとうございました!)。

最初は「タイトルロゴだけ」のつもりだったんですが、やっぱりTOPページは集客する上で超大事だろうってことで、TOPページのデザインもお願いすることにしました。

自分で作ったデザインをお渡しして、「これをいい感じにしてください!」という超ざっくりとしたオーダーだったにも関わらず、マジでヤバいクオリティで仕上げてくださいました。テンション爆上がり。

ESlint, Prettierの導入

これはどうしても入れたかったヤツらです。入れておけば、インデントやクォーテーション(シングル or ダブル)などの細かい点は気にせずとも保存時に自動で書式を統一して整形くれるので、コーディングが非常にラクになります。

細かい設定に関してはもうソースを見てください(雑)

.eslintrc.js.prettirerc, webpack.mix.js あたりが設定ファイルになります。

ESlintはreactに対応するように色々と設定の変更が必要です。

react用のプラグインを入れたそのままの設定だと、正しい書き方をしているはずなのにエラーが出たり、逆に「そのエラーは吐かなくてもいいよ」てのも多かったりするので、個別で設定を変更していきます。

VSCodeだと、ESlintでエラーになっている箇所は波線で示してくれて(黄色は警告、赤はエラー)、波線にカーソルを合わせるとエラーの内容を表示してくれます。

スクリーンショット 2020-12-09 21.07.32.png

今回で言うと、ESlintの@typescript-eslint/explicit-modele-boudary-typesに関するエラーだよってことを教えてくれています。

そのまま@typescript-eslint/explicit-modele-boudary-typesの部分にカーソルを合わせると、外部サイトへのリンクになっています。

スクリーンショット 2020-12-09 21.13.10.png

スクリーンショット 2020-12-09 21.05.42.png

開いてみると、@typescript-eslint/explicit-modele-boudary-typesのリファレンスへ飛びます。

スクリーンショット 2020-12-09 21.06.08.png

こんな感じでエラーの内容を確認しては、

  1. エラーを解消するようにソースを変更
  2. 設定を変更し、エラーが出ないようにする

などしてエラーを消していく作業をひたすら地道に行っていきます。発狂しそうになります。

保存時に --fix する設定にしていますが、全部のエラーを解消してくれるわけではありません。なので、結構手動で修正しないといけない。

あとで入れるTypeScriptの設定も入っているので、Laravel(laravel-mix) x React × TypeScript × ESlint × Prettire の場合は、このプロジェクトの環境をそのまま使えばとりあえず動くと思います。( 他のソースも含めて気になる方は、kiwatchi1991/folicolle -Github をご覧ください)

.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es6: true,
    node: true,
        jest: true,
    mocha: true
    },
  extends: [
    "airbnb",
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:prettier/recommended",
    "plugin:import/errors",
    "plugin:import/warnings",
    "plugin:import/typescript",
    "prettier/@typescript-eslint",
    "prettier/react",
    ],
  globals: {
    Atomics: "readonly",
    SharedArrayBuffer: "readonly",
      "__DEV__": true,
    "React": false
    },
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaFeatures: {
      jsx: true
        },
    project: "./tsconfig.json",
    sourceType: "module"
    },
  plugins: [
    "@typescript-eslint",
    "import",
    "prettier",
    "react",
    ],
  root: true,
    rules: {
    "react/jsx-filename-extension": [1, { "allow": "as-needed" }],
    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
    "newline-before-return": "error",
    "no-console": "off",
    "no-continue": "off",
    "require-yield": "error",
    "spaced-comment": [
      "error",
      "always",
            {
        markers: ["/"
                ],
            },
        ],
    "no-use-before-define": "off",
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": ["off"],
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/explicit-member-accessibility": "off",
    "@typescript-eslint/no-unnecessary-type-assertion": "error",
    "@typescript-eslint/no-var-requires": "off",
    "react/jsx-filename-extension": [
      "error",
            {
        extensions: ["jsx", "tsx"
                ]
            }
        ],
    "react/jsx-props-no-spreading": [
      "warn",
            {
        custom: "ignore",
            },
        ],
    "react/prop-types": "off",
    "react/require-default-props": ["off", { forbidDefaultForRequired: false, ignoreFunctionalComponents: false }],
    "react/no-unused-prop-types": ["off", {}],
    "react/default-props-match-prop-types": ["off", { "allowRequiredDefaults": false }],
    "react/jsx-uses-react": "error",
    "react/jsx-uses-vars": "error",
    "import/extensions": [
      "error",
      "always",
            {
        js: "never",
        jsx: "never",
        ts: "never",
        tsx: "never"
            }
        ],
    },
  settings: {
    "import/parsers": {
      "@typescript-eslint/parser": [".ts", ".tsx"
            ],
        },
    "import/resolver": {
      node: {
        extensions: [".js", "jsx", ".ts", ".tsx"
                ],
        paths: ["src"
                ],
            }
        },
    react: {
      version: "detect"
        }
    },
};
.prettirerc
{
  "printWidth": 120,
  "useTabs": false,
  "semi": true,
  "singleQuote": false,
  "trailingComma": "es5",
  "bracketSpacing": true,
  "jsxBracketSameLine": false
}

webpack.mix.js
const mix = require("laravel-mix");
require("laravel-mix-react-css-modules");
require('laravel-mix-stylelint');

mix.webpackConfig({
    module: {
        rules: [
            {
                test: /\.scss/,
                enforce: "pre",
                loader: "import-glob-loader",
            },
            // ==============================
            // ESlintの設定はここから ↓↓↓
            // ==============================
            {
                enforce: "pre",
                exclude: /node_modules/,
                loader: "eslint-loader",
                test: /\.(js|jsx)?$/,
                options: {
                    fix: true,
                    cache: false,
                },
            },
            // ==============================
            // ESlintの設定はここまで ↑↑↑
            // ==============================
        ],
    },
});
/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.ts("resources/js/app.tsx", "public/js")
    .sass("resources/sass/app.scss", "public/css")
    .stylelint({
        files: ['**/*.scss'],
    })
    .reactCSSModules()
    .sourceMaps()
    .browserSync({
                https: false, 
        files: ["./resources/**/*", "./app/**/*", "./config/**/*", "./routes/**/*", "./public/**/*"],
        proxy: {
            target: "http://127.0.0.1:8000", 
        },
        open: true,
        reloadOnRestart: true,
    });

スタイリングをemotionからCSS Modules

今回の開発を始めるにあたって調査をしていると、どうやらReactプロジェクトのスタイリングには、ノーマルなcss/scssだけでなくstyled-componentsemotionといった、css-in-jsというジャンルのスタイリング手法が存在するということを知りました。

そしてこれは僕の調査が甘いからかもしれませんが、css-in-jsを使うことが「ナウイケてるらしいぞ」という情報が多かったので、ワケもわからず一番人気と思われる styled-componentsを導入することにしました。

しかし、実際に導入してから気づいたんですが、styled-componentsの書き方はマークアップに慣れた人間からすると非常に「直感的でない」んですね。htmlのタグがコンポーネントの名前になるのでDOM構造がぱっと見わかりづらいし、cssもキャメルケースで書かなきゃいけない。

margin-top: 10px;  //普通のcss記法
marginTop : 10px;  //styled-componentsの記法

これだと確実に作業効率落ちる、、、と思い色々調べていると、普通のcssのように書けるemotionというライブラリがあると知り、emotionを導入してここまで認証系3ページのスタイリングをしてきました。

ただやっぱり使いづらいところはあって、シンプルにコンポーネント内にスタイルのコードを書くのでコード量が多くなって見通しが悪くなります。それに、emotionでも「styled-components風の記述ができる」というのが大きな強みのように語られていて、「styled-componentsの何がそんなにいいんだ?」てのがずっと疑問でした。

この疑問を会社のリーダーとの1on1でぶつけてみたら、返ってきた答えが予想外すぎて目から鱗がボロボロと剥がれ落ちました。。

styled-componentsは、我々のようにマークアップに抵抗がない人間からすると『直感的でなくて使いづらい』というのは本当にそうだと思う。あれはたぶんcssを嫌う人たちによって作られたんだ(あくまで予想)。
だから、普通のcss/scssの方が使いやすいのなら、そっちを使えばいいんだよ。別にstyled-componentsが優れているわけじゃない。」

なるほどそうなのか。。。
自分の中にあった違和感は正しかったし、自信を持ってcssを書けばいいんだと背中を押されたような気分でした。

というわけで、styled-componentsemotionも無理に使う必要は全くないってことが分かったので、CSS設計をしなくてよいコンポーネント単位のスタイリングCSS ModulesでシンプルにSCSSを使うことにしました。

こんな感じでモジュール呼び出して使います。

Header/index.tsx
import React from "react";
const styles = require("./index.modules.scss");

const Header = () => {
    return (
        <div className={styles.header}>
            // 中略
        </div>
    );
};

export default Header;
Header/index.modules.scss
.header {
    height: 54px;
}

普段からある程度マークアップに慣れている人間からすると、SCSS普通に書くのが結局一番早い。CSS ModulesでCSS設計しなくてよくなるのは、それだけでマークアップのストレスを5割減させてくれる。

GithubActionsを使った自動デプロイ

まだまだリリースできる段階にはほど遠いですが、人に触ってもらいたくてとりあえずサーバーにデプロイしました。

これまでもいくつかサービス作って本番デプロイを経験しましたが、「ローカルで更新」→「リモートにプッシュ」→「サーバーにssh接続してログイン」→「リモートからプル」という一連の流れを毎回手動で行っており、非常に面倒臭いなと感じていました。

そこで今回はどうしても「自動デプロイ」を導入したかったので、巷で流行っているっぽいCircleCIにチャレンジしてみました。

ただ、慣れないインフラ寄りの知見は乏しく、一人でやってもうまくいかず、CircleCIでの自動デプロイ環境を構築するには至りませんでした。。

しかし諦めません。

「もっと簡単な他の方法はないのか?」とさらに調査を進めると、「『masterブランチをリモートにプッシュしたら自動デプロイする』程度のものならGithubActionsが最も簡単に実現できる」との情報を得たので、GithubActionsでの実装に切り替えることに!

、、、しかし、「最も簡単」なGithubActionsですら自力で実装することができず、ここで完全に諦めて「MENTA」を使うことにしました。

MENTAは、「メンターが出品しているサービスを見つけて買う」だけでなく、「自分でメンター募集をかけて、見つけてもらう」という使い方があります。

僕が実際に投稿したメンター募集はこちら
スクリーンショット 2020-12-09 23.16.45.png

メンター募集をかけて待っていると、内容と予算を見て「助けてやってもイイゼ〜」と思った方がいたら手を挙げてくれます。(この時は、予算2,000円で募集しましたが結局1,000円でやってもらえました。)

これくらいの内容だと、詳しい人からすれば朝飯前

DMで自分の今の状態(やりたいこと・今わかっていること・分からなくて行き詰まっているところ)を伝えると、「このサイト参考にしてみてください」とリンクを1つ共有してくださり、、、、なんと、実装できちゃいました。

DMで1往復やりとりしただけです。。

自分でも散々調べたのに見つけられなかったサイトでした。やはりつよつよな人は情報へのアクセスが素早く的確です。

こうして無事にGithubActionsを使った自動デプロイ環境の構築ができました。

設定はこちら

Laravelの認証テスト

「今回の開発ではしっかりテストも書くぞ!」ということで、見よう見まねでテスト書いてみました。

認証系のテストを書いたんですが、まとまった記事がなかったので自分でQiita記事書いちゃいました!
【Laravel テスト】フォームリクエスト 複数項目バリデーションのテストコードを書いてみた -Qiita

SNSログイン

LaravelにはSocialiteという便利なOAuth用パッケージがあるので、こいつをインストールして使います。

Socialiteの使い方を世界一丁寧に解説したがタイトル通りめちゃくちゃ分かりやすく解説してくれています。Twitterログインだけならこの記事で実装できちゃうと思います。

(参考)

このあたりの記事が分かりやすかった気がします。

(余談)Zennのログイン画面

今話題のZennのログイン画面って、Googleログインオンリーなんです。

スクリーンショット 2020-12-10 1.03.40.png

メチャクチャ斬新だなーと思ったとともに、メチャクチャ「あり」だなと思いました。

これだけ世の中にサービスが溢れている中で、「そのサービス用にメアドでアカウント登録したのか、TwitterだったかGoogleだったかFacebookだったか」なんていちいち覚えてられないですよね?

選択肢が多ければ多いほど迷います。「毎回おれはTwitterで登録している!」と決め打ちの人もいるとは思いますが、まあ少数でしょう。

Zennは選択肢を一つにすることでその迷いをなくすことができています。

「これはぜひ取り入れたい!」と思ったので、せっかくフロントのバリデーションまで実装したユーザー登録/ログインのフォームをきっぱりと捨てて、ソーシャルオンリーにしてユーザーに迷いを与えない仕様にしていきます。

TypeSctipt導入

「やっぱりモダンフロントエンドにTypeScriptは欠かせないでしょ!」ってことで、導入。

認証の数画面をすでに実装し終えていた状態で、しかもプロジェクト全体に一気にTypeScriptを入れたので、コンパイル時のエラーが100近く出ました(絶望)。

TypeScript自体初めてだったので全部の型を適切に定義することは一旦諦め、:anyを多用してなんとか全エラーを解消するまで8時間かかりました(あかん)。

開発途中のプロジェクトに一気に全体適用するなんて、良い子は絶対にマネしないでね。

(参考)
TypeScriptの学習には、Udemyの講座を使いました。

フロントテストの環境構築

業務へのキャッチアップも兼ねている今回の個人開発。

いわゆるモダンフロントエンド盛り盛りセットのレベルまでもっていかなければならないので、フロントエンドのテストも外せません。

当時のブログ 86日目 個人開発記14 -フロント環境構築完了-で、環境構築に苦労した話やbabel.config.jsの設定について書いてますが、今見てもなぜこの設定になってるのか思い出せない笑

とりあえず、これで動くみたいですよ。

babel.config.js
module.exports = {
    presets: [
        [
            "@babel/preset-react",
            {
                modules: "false",
                useBuiltIns: "usage",
                targets: "> 0.25%, not dead",
            },
        ],
    ],
    env: {
        test: {
            presets: [["@babel/preset-env", { targets: { node: "current" } }]],
        },
    },
};

とりあえずテスト動く状態にはなっているのですが、「そもそも何をテストするのか?」がイマイチ分かっていないので、実際にコードを書くのはこれからですネ。

README書いてみた

各所でGithubリポジトリのURLを共有しているので、「せっかく見てもらうのならREADMEをちゃんと書こう」と思い、書いてみました。
kiwatchi1991/folicolle - Github

こんな感じ
スクリーンショット 2020-12-10 21.22.00.png
スクリーンショット 2020-12-10 21.22.11.png

「ポートフォリオ一覧」表示ロジックの実装

ようやくメイン部分の実装らしい実装。

seeder, factory, fakerの理解

まずはCRUDRのみ実装していきます。seederを使ったダミーデータの生成で、factoryfakerの理解が深まりました。

メインコンテンツのモデルProductに、一対一でリレーションするUser多対多でリレーションするCategoryを紐づけた状態でダミーデータを作るための設定はこんな感じ。

(Productをcreateする際に、新規でUserも作成してそのiduser_idに格納、その後Categoriesテーブルに登録してあるカテゴリーの中からランダムで7つカテゴリーを選んで登録する という流れ。)

database/seeds/ProductsSeeder.php
<?php

use App\Category;
use Illuminate\Database\Seeder;
use App\Product;

class ProductsSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $products = factory(Product::class, 10)->create([
            'title' => "タイトル",
            'description' => 'これは説明です。これは説明です。これは説明です。これは説明です。これは説明です。これは説明です。これは説明です。これは説明です。これは説明です。',
            'body' => "これは記事本文ですこれは記事本文ですこれは記事本文ですこれは記事本文ですこれは記事本文ですこれは記事本文ですこれは記事本文ですこれは記事本文ですこれは記事本文です"
        ]);

        //各プロダクトに、ランダムで3つカテゴリーを紐づける
        $categories = Category::all();

        $count = 7;
        $arr = [];
        foreach ($products as $product) {
            for ($i = 0; $i < $count; $i++) {
                $r = mt_rand(0, count($categories) - 1);
                $arr[$i] = $categories[$r]->id;
            }
            $product->categories()->sync($arr);
        }
    }
}
database/factories/ProductFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Product;
use App\User;
use Faker\Generator as Faker;

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/

$factory->define(Product::class, function (Faker $faker) {
    return [
        'title' => $faker->text(10),
        'description' => $faker->text(100),
        'body' => $faker->text(255),
        'user_id' => factory(User::class)->create()->id,
        'img' => $faker->text(40),
    ];
});

database/factories/UserFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\User;
use Faker\Generator as Faker;
use Illuminate\Support\Str;

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/

$factory->define(User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'comment' => '僕は〇〇な人です',
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
});

(参考)
「fakerのメソッド一覧」みたいなのは公式リファレンスが見当たらなかったので、こちらのQiita記事が参考になりました。(見つけられなかっただけかも。知っていたら教えてください。。)

Githubのissueでタスク管理始めました

現場ではGithubを使っていないので、Githubの使い方に慣れる目的でissueでのタスク管理を始めました。

立てたissueがcloseしていくと「確実に前に進んでいるのが目に見える」ので、精神衛生上非常に良いですね。

アプリ完成まで長くモチベーションを維持していくうえでメンタルの健康はとても大切。

(参考)

Stylelintの導入

「SCSSも自動で整形してほしい!」と思い導入。自動整形いいですねぇ〜。

自分で書いたブログ記事がよくまとまっているので、参考にしてみてください。

(参考)

「ポートフォリオ一覧」ページのコーディング

見た目の部分。マークアップですね。

「全くCSSが書かれていない」な0→1フェーズは結構しんどいけど、だんだん形になってくると無機質なデータの集合体に命が吹き込まれていくような感じに思えてきてイイですね(?)

CSS書くのもそんなに嫌いじゃない。

「ポートフォリオ詳細」表示ロジックの実装

特にコメントはないです。

SQLクライアントツールを使おう → やっぱりまずはターミナルからでしょ!

これまでDBはMySQLしか使ったことなく、クライアントツールはphpMyAdminを使っていました。

たまたま見た【2020年版】フロントエンドのおすすめMac無料ツールで、TablePlusというクライアントツールが紹介されていたので、試しに使ってみよう!

、、、と思ってたんですが、「クライアントツールもいいけど、まずはターミナル操作ができるようになった方がいい。現場ではターミナルでがんがんDB触るよ!」という情報をフォロワーさんからいただいたので、素直な僕はターミナル操作を覚えることにしました。

SQL文を書くなんて、素のPHPでCRUDアプリ作ったとき以来だから完全に忘れてる。。
徐々に思い出していきます。

まとめ

超ボリューミーな内容になりました。

「なんかよく分からんけど、苦労してるっぽい」のは存分に伝わったんじゃないかと思います。

でもこれが、「実務ではReactもLaravelも触っていない歴1年ちょっとのエンジニア」の個人開発のリアルです。ドキュメンタリー

おわりに

Qiitaは不定期更新でやっておりますが、Twitter ( @kiwatchi1991 ) では日々感じたことや細かい試行錯誤の様子などを発信しております。

また、ブログを毎日更新していて(今は110日を超えたあたり)、数日に一度個人開発に関する記事を書いています。

各論はブログの方が細かく書いてたりするので、よかったらこちらもぜひのぞいてみてください。

それではまた次回!

8
1
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
8
1