[Go×Supabase]ReactとGoを用いてDB連携をしてみる

## はじめに
初めましての人もそうでない人もこんにちは！

あまりにソシャゲに課金したすぎてアフィリエイト的な何かで小遣い稼ぎをしたいなーと思っている今日この頃です。

今回はフロントエンドにReact×TypeScript、バックエンドにGo×Supabaseを使ってフロントとバックのDB連携をしてみたいと思います！

## 今回作るもの
今回は絵文字を登録していつでも簡単にコピペできるようなwebアプリを作成しようと思います！

## DB構造
```
テーブル名：Emojis
カラム：　id => INT型、　character => TEXT型
```

## 主なディレクトリ構成
```
.emoji-app/
├── backend/
│    ├── main.go
│    ├── .env
│    ├── go.mod
│    └── go.sum
└── frontend/
     ├──src/
    ...  ├──components/
         │    ├── EmojiApp.css
         │    └── EmojiApp.tsx
         ├── App.tsx
        ...
```

## 準備物
- Supabaseのプロジェクト立ち上げ
- プロジェクトパスワードの保存

## 作ってみよう！
### フロントエンド
とりあえず適当にプロジェクト用のフォルダを作成してそのディレクトリ内にReactを立ち上げます！

```
npx create-react-app frontend --template typescript
cd frontend
npm install axios
```

**App.tsxの編集**
srcフォルダの中にあるApp.tsxを以下のように置き換えてください！
```src/ App.tsx
import React from 'react';
import EmojiApp from './components/EmojiApp';

const App: React.FC = () => {
  return (
    <div className="App">
        <EmojiApp />
    </div>
  );
};

export default App;
```

**componentsフォルダの作成**
componentsフォルダを作成してそのフォルダ内にEmojiApp.tsxを作成してください！

```components/ EmojiApp.tsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './EmojiApp.css';

interface Emoji {
  id: number;
  character: string;
}

const EmojiApp: React.FC = () => {
  const [emojis, setEmojis] = useState<Emoji[] | null>(null);
  const [newEmoji, setNewEmoji] = useState('');
  const [copiedEmoji, setCopiedEmoji] = useState<string | null>(null);

  useEffect(() => {
    fetchEmojis();
  }, []);

  const fetchEmojis = async () => {
    try {
      const response = await axios.get<Emoji[]>('http://localhost:8080/emojis');
      setEmojis(response.data);
    } catch (error) {
      console.error('絵文字の取得中にエラーが発生しました:', error);
      setEmojis([]);
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await axios.post('http://localhost:8080/emojis', { character: newEmoji });
      setNewEmoji('');
      fetchEmojis();
    } catch (error) {
      console.error('絵文字の追加中にエラーが発生しました:', error);
    }
  };

  const handleCopy = (emoji: string) => {
    navigator.clipboard.writeText(emoji);
    setCopiedEmoji(emoji);
    setTimeout(() => setCopiedEmoji(null), 2000);
  };

  const handleDelete = async (id: number) => {
    try {
      await axios.delete(`http://localhost:8080/emojis/${id}`);
      setEmojis((prevEmojis) => prevEmojis ? prevEmojis.filter(emoji => emoji.id !== id) : null);
    } catch (error) {
      console.error('絵文字の削除中にエラーが発生しました:', error);
    }
  };

  return (
    <div className="emoji-app">
      <div className="container">
        <h1>絵文字コピーアプリ</h1>
        <form onSubmit={handleSubmit} className="emoji-form">
          <input
            type="text"
            value={newEmoji}
            onChange={(e) => setNewEmoji(e.target.value)}
            placeholder="絵文字を入力してください"
          />
          <button type="submit">追加</button>
        </form>
        {copiedEmoji && (
          <div className="copy-message">
            {copiedEmoji} をコピーしました！
          </div>
        )}
        <div className="emoji-grid">
          {emojis === null ? (
            <p>読み込み中...</p>
          ) : emojis.length === 0 ? (
            <p>絵文字がありません。</p>
          ) : (
            emojis.map((emoji) => (
              <div key={emoji.id} className="emoji-item">
                <button
                  onClick={() => handleCopy(emoji.character)}
                  className="emoji-button"
                >
                  {emoji.character}
                </button>
                <button
                  onClick={() => handleDelete(emoji.id)}
                  className="delete-button"
                >
                  ×
                </button>
              </div>
            ))
          )}
        </div>
      </div>
    </div>
  );
};

export default EmojiApp;
```

そしてEmojiApp.cssを作成してデザインに手を加えます！
```components/ EmojiApp.css
.emoji-app {
    min-height: 100vh;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    display: flex;
    justify-content: center;
    align-items: center;
    font-family: 'Arial', sans-serif;
  }
  
  .container {
    background-color: white;
    border-radius: 10px;
    padding: 2rem;
    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
    width: 90%;
    max-width: 600px;
  }
  
  h1 {
    color: #4a5568;
    text-align: center;
    margin-bottom: 1.5rem;
    font-size: 2rem;
  }
  
  .emoji-form {
    display: flex;
    margin-bottom: 1.5rem;
  }
  
  .emoji-form input {
    flex-grow: 1;
    padding: 0.5rem;
    font-size: 1rem;
    border: 1px solid #e2e8f0;
    border-radius: 4px 0 0 4px;
  }
  
  .emoji-form button {
    background-color: #4299e1;
    color: white;
    border: none;
    padding: 0.5rem 1rem;
    font-size: 1rem;
    cursor: pointer;
    transition: background-color 0.3s;
    border-radius: 0 4px 4px 0;
  }
  
  .emoji-form button:hover {
    background-color: #3182ce;
  }
  
  .copy-message {
    background-color: #48bb78;
    color: white;
    padding: 0.5rem;
    text-align: center;
    border-radius: 4px;
    margin-bottom: 1rem;
  }
  
  .emoji-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
    gap: 0.5rem;
  }
  
  .emoji-button {
    background-color: #edf2f7;
    border: none;
    font-size: 1.5rem;
    padding: 0.5rem;
    cursor: pointer;
    transition: background-color 0.3s, transform 0.1s;
    border-radius: 4px;
  }
  
  .emoji-button:hover {
    background-color: #e2e8f0;
    transform: scale(1.05);
  }
  .emoji-item {
    position: relative;
  }
  
  .delete-button {
    position: absolute;
    top: -5px;
    right: -5px;
    background-color: #e53e3e;
    color: white;
    border: none;
    border-radius: 50%;
    width: 20px;
    height: 20px;
    font-size: 12px;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.3s;
  }
  
  .emoji-item:hover .delete-button {
    opacity: 1;
  }
```

### バックエンド
ターミナルのemoji-appディレクトリから以下のようにしてバックエンドの環境を構築してください！
```
mkdir backend
cd backend
touch main.go
touch .env
go mod init backend
go get github.com/gorilla/mux
go get github.com/joho/godotenv
go get github.com/lib/pq
go get github.com/rs/cors
```

**main.goの編集**
それではバックエンドのコーディングをしていきたいと思います！
```backend/ main.go
package main

import (
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "strconv"

    "github.com/gorilla/mux"
    "github.com/joho/godotenv"
    _ "github.com/lib/pq"
    "github.com/rs/cors"
)

type Emoji struct {
    ID        int    `json:"id"`
    Character string `json:"character"`
}

var db *sql.DB

func main() {
    if err := godotenv.Load(); err != nil {
        log.Fatal("Error loading .env file")
    }

    var err error
    db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal("Error connecting to the database:", err)
    }
    defer db.Close()

    log.Println("Successfully connected to the database")

    r := mux.NewRouter()
    r.HandleFunc("/emojis", getEmojis).Methods("GET")
    r.HandleFunc("/emojis", addEmoji).Methods("POST")
    r.HandleFunc("/emojis/{id}", deleteEmoji).Methods("DELETE")

    c := cors.New(cors.Options{
        AllowedOrigins: []string{"http://localhost:3000"},
        AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"},
        AllowedHeaders: []string{"*"},
        AllowCredentials: true,
    })

    handler := c.Handler(r)

    log.Println("Starting server on :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}

func getEmojis(w http.ResponseWriter, r *http.Request) {
    rows, err := db.Query("SELECT id, character FROM emojis")
    if err != nil {
        log.Println("Error querying database:", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer rows.Close()

    var emojis []Emoji
    for rows.Next() {
        var emoji Emoji
        if err := rows.Scan(&emoji.ID, &emoji.Character); err != nil {
            log.Println("Error scanning row:", err)
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        emojis = append(emojis, emoji)
    }

    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(emojis); err != nil {
        log.Println("Error encoding response:", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

func addEmoji(w http.ResponseWriter, r *http.Request) {
    var emoji Emoji
    if err := json.NewDecoder(r.Body).Decode(&emoji); err != nil {
        log.Println("Error decoding request body:", err)
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    if _, err := db.Exec("INSERT INTO emojis (character) VALUES ($1)", emoji.Character); err != nil {
        log.Println("Error inserting into database:", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusCreated)
}

func deleteEmoji(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
    w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS")
    w.Header().Set("Access-Control-Allow-Headers", "*")

    if r.Method == "OPTIONS" {
        w.WriteHeader(http.StatusOK)
        return
    }

    vars := mux.Vars(r)
    id, err := strconv.Atoi(vars["id"])
    if err != nil {
        log.Println("Error parsing id:", err)
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    result, err := db.Exec("DELETE FROM emojis WHERE id = $1", id)
    if err != nil {
        log.Println("Error deleting from database:", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    rowsAffected, err := result.RowsAffected()
    if err != nil {
        log.Println("Error getting rows affected:", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    if rowsAffected == 0 {
        http.Error(w, "Emoji not found", http.StatusNotFound)
        return
    }

    w.WriteHeader(http.StatusNoContent)
}
```

最後に作成した.envファイルを編集します！
```.env
DATABASE_URL="postgresql://{supabaseのDatabase Settingsからコピペしてください}"
```

## 実行しよう
それでは実行してみましょう！
まずはバックエンドから実行してみましょう！
```
go run .
```
これ初めて知った時結構びっくりしたんですけどどうやら最後main.goと入力しなくてもピリオドで実行することができるみたいです！
もちろんケースバイケースなので絶対ではありませんし、今回みたいな簡単なアプリには必要はありません。

ぜひこの場では知識としてこのような実行方法もあるんだよ程度で共有できたらなと思いました！

**参考記事**

https://qiita.com/twrcd1227/items/00c2802a2131c849fa5c

次にフロントエンド側でも実行してみます！
```
npm start
```

これで全ての実行が終わり使えるようになっているはずです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/ee101bbc-78d8-3a35-ba36-17626088a444.png)

できました！！
これに『ᓀ‸ᓂ』『⎛ಲළ൭⎞』これらの絵文字を追加していきたいと思います！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/7187c88a-2647-edae-12a3-ec627a6bf32a.png)

できました！それでは作りすぎたので一個消したいと思います！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/41b5b1f2-adff-55c3-09da-8534185a4560.png)

消えました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/2c2114dd-2ef0-04ca-600f-1828e08b4fc4.png)

追加した顔文字をクリックすると・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631203/97c70198-d516-5d6d-b697-9ddf3462c972.png)

ちゃんとコピーされたのでいつでもコピペして使えそうです！

## 最後に
今回はDB構造もかなり適当で1テーブルの2カラムしかないという適当ぶりですが多めにみてくださいw

今回の記事はいかがだったでしょうか？
またどこかの記事でお会いしましょう！

## GithubURL

https://github.com/FrohleinYoshie/emojiDB