참고
간단한 리액트 게시판 만들기 : WYSIWYG ckeditor - by FaLaner
새로운 프로젝트에서 REACT와 GO를 연동한 웹 개발을 계획함에 따라 REACT와 GO를 연동하는 법을 익힐 겸, 간단한 토이 프로젝트를 진행하기로 했다.
REACT를 다뤄본 적이 없어 다른 분이 진행한 토이 프로젝트를 기반으로 작업을 시작했다.
해당 프로젝트의 백엔드는 Node.js의 Express 프레임워크로 구현되었는데 이를 GO 코드로 리팩토링 해보려 한다.
데이터베이스로는 MySQL을 사용했다.
사용자가 게시판에 글을 적고 입력 버튼을 누르면, 해당 내용이 백엔드 서버를 통해 MySQL DB에 저장된다. 이 페이지는 랜더링될 때마다 DB에 저장된 data를 백엔드 서버를 거쳐 불러온다.
프론트엔드 구현에 관한 내용은 기존 프로젝트와 동일하게 진행되었으므로 위 포스트를 참고하는 것을 권장한다
이 포스트에서는 Express로 구현된 백엔드 서버를 Go로 재구현하는 것에 초점을 두려 한다
프로젝트 폴더에서 client 폴더와 server라는 폴더를 생성하여 REACT 관련 내용을 client 폴더에 옮겼다. 그 후, server라는 폴더에 main.go
파일을 추가했다.
다음으로, 간단한 go 서버를 구현했다.
<main.go>
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", index)
http.ListenAndServe(":8080", nil)
}
func index(w http.ResponseWriter, req *http.Request) {
fmt.Println("server is running...")
}
위는 8080포트를 통해 서버를 오픈하는 코드이다. 서버를 오픈하면 콘솔에 server is running...
이라는 문구가 출력된다.
❗ 설치 도중, Type and Networking을 설정하는 단계에서 3306 Port 충돌이 일어나면 아래 포스트를 통해 문제를 해결할 수 있다.
mysql -u root -p
❗ 만약, mysql이라는 명령어를 찾을 수 없다는 에러가 뜨면 아래 포스팅을 참고하면 된다.
https://m.blog.naver.com/c6369/220625848670
CREATE DATABASE simpleboard;
❗ MySQL은 코드 끝에 세미콜론을 붙여야 한다. 만약 코드 입력 시에 화살표가 등장하면, 세미콜론을 입력하지 않아 다음 내용을 더 입력하라는 의미이므로 입력을 마치고 싶으면 세미콜론을 추가로 입력하면 된다.
use simpleboard;
CREATE TABLE simpleboard (
-> idx INT(10) NOT NULL AUTO_INCREMENT,
-> title CHAR(100) NOT NULL,
-> content TEXT NOT NULL,
-> PRIMARY KEY(idx)
-> );
❗ 이때, use simpleboard
를 꼭 입력해야 한다. 그렇지 않으면, 대상 db를 찾을 수 없다는 에러가 뜬다.
<main.go>
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func main() {
http.HandleFunc("/", get)
http.ListenAndServe(":8080", nil)
}
var db *sql.DB
var err error
func get(w http.ResponseWriter, req *http.Request) {
db, err = sql.Open("mysql", "root:1234@tcp(127.0.0.1:3306)/simpleboard") // 1
if err != nil {
log.Fatalln(err)
}
conn, err := db.Query(
"INSERT INTO simpleboard (title, content) VALUES (?,?)", "LOL", "재밌다ㅋㅋ") // 2
if err != nil {
log.Fatalln(err)
}
conn.Close() // 3
fmt.Printf("Connection 연결 종료: %+v\n", db.Stats())
db.Close()
fmt.Printf("DB 연동 종료: %+v\n", db.Stats())
}
host명:비밀번호@프로토콜(Ip:Port)/database명
sql.Open
은 실제 DB Connection을 Open하지 않는다db.QueryRow
를 사용한다db.Query
로도 가능?
(Placeholder)를 통해 Parameterized Query를 사용한다 ?
에는 파라미터로 넘긴 각 문자열(LOL
, 재밌다
)이 대입된다defer
를 통해 프로세스가 진행된 후 닫을 수 있도록 지연시킨다localhost:8080
에 접속이 과정을 진행함에 앞서, AJAX와 Axios의 개념에 대해 짚고 넘어가려 한다.
AJAX
AJAX란 비동기 자바스크립트와 XML (Asynchronous JavaScript And XML)을 말합니다. 간단히 말하면, 서버와 통신하기 위해 XMLHttpRequest 객체를 사용하는 것을 말합니다.
- MDN Web Doc
더 자세한 설명은 MDN 문서를 통해 확인 가능하다. 쉽게 말해, AJAX는 클라이언트와 서버의 데이터 통신을 위한 기능이라고 보면 된다.
클라이언트에서 서버에 데이터를 요청하려면 API(GET
, POST
등)가 필요한데 이를 사용하기 위해서는 자바스크립트의 내장 객체인 XMLRequest나 다른 HTTP Client가 필요하다.
일반적으로 HTTP Client로써 Fetch API
를 많이 사용하지만 REACT에서는 주로 Axios
를 사용하는 경향이 있다. 두 HTTP Client의 차이점을 알고 싶다면 다음 포스트를 참고하면 된다.
https://velog.io/@shin6403/React-axios%EB%9E%80-feat.-Fetch-API
이제, GO로 만든 서버와 REACT로 만든 클라이언트를 연동할 차례이다. 이를 위해, client 폴더에 axois 모듈을 설치하고 다음 코드를 추가한다.
<App.js>
import Axios from 'axios';
...
const submitReview = ()=>{
Axios.post('localhost:8080/api/insert', { // 1
title: gameContent.title,
content: gameContent.content
}).then(()=>{
alert('등록 완료!');
})
};
...
<button
className="submit-button"
onClick={submitReview} // 2
>입력</button>
다시, 서버 프로그램으로 돌아와 Axios.post
에 대응하는 POST 핸들러를 추가한다.
import (
"encoding/json"
)
func main() {
http.HandleFunc("/api/insert", insert) // 1
http.ListenAndServe(":8080", nil)
}
var db *sql.DB
var err error
type User struct { // 2
Title string
Content string
}
func insert(w http.ResponseWriter, req *http.Request) {
var u User
err = json.NewDecoder(req.Body).Decode(&u) // 3
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
title := u.Title
content := u.Content
db, err = sql.Open("mysql", "root:0000@tcp(127.0.0.1:3306)/simpleboard")
if err != nil {
log.Fatalln(err)
}
// 4
conn, err := db.Query("INSERT INTO simpleboard (title, content) VALUES (?,?)", title, content)
if err != nil {
log.Fatalln(err)
}
conn.Close()
fmt.Printf("Connection 연결 종료: %+v\n", db.Stats())
db.Close()
fmt.Printf("DB 연동 종료: %+v\n", db.Stats())
}
Axios.post('localhost:8080/api/insert', ...)
으로부터 받은 요청에 응답하기 위한 함수이다{}
으로만 인식된다Axios.post
로부터 넘어오는 데이터는 JSON 형식으로 되어있다req.body
는 request message의 body 부분을 의미하며, 이로부터 사용자 input을 추출한다User
타입의 u
변수에 담긴다위 과정까지 마치면, 다음의 절차를 수행한다.
main.go
)을 실행한다ex:) yarn start
)를 실행한다그 결과.....?!
❗❗아무 것도 동작하지 않는다!!❗❗
왜 아무 것도 동작하지 않을까?
React 서버를 실행하면서 열린 브라우저에서 오른쪽 마우스를 클릭하고 검사를 누르면 다음과 같은 에러를 확인할 수 있다.
이는 CORS라는 정책으로부터 비롯된 에러이다.
CORS 정책이란 무엇인가?
교차 출처 리소스 공유 (CORS)
교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.
교차 출처 요청의 예시: https://domain-a.com의 프론트 엔드 JavaScript 코드가 XMLHttpRequest를 사용하여 https://domain-b.com/data.json을 요청하는 경우.
- MDN Web Doc
더 자세한 설명은 MDN 문서를 통해 확인할 수 있다.
요컨대, 서로 다른 출처로부터 리소스를 주고 받는 경우에 보안상의 이유로 CORS 정책에 따라 코드를 작성해야 된다는 말이다.
앞서, React 서버가 사용하는 포트(3000)와 백엔드 서버가 사용하는 포트(8080)가 다르다고 언급한 적이 있다. 즉, REACT와 백엔드 서버는 서로 다른 출처로부터 리소스를 공유하게 되므로 이들이 통신할 때에는 CORS 처리가 필요하다.
Go에서의 CORS는 패키지를 통해 처리가 가능하다.
go get github.com/rs/cors
CORS 패키지와 관련된 정보는 CORS gihub에서 확인 가능하다.
❗ CORS 설치 후, import 에러가 발생할 수 있다. 이는 관련 패키지가 설치되지 않아 발생하는 에러이므로 examples과 wrapper 두 폴더를 과감히 삭제해준다. 두 폴더가 없더라도 CORS 패키지는 정상적으로 동작한다. 단, gin 프레임워크가 설치되어 있다면 wrapper 폴더는 남겨두어야 한다.
import (
"github.com/rs/cors"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/insert", insert)
mux.Handle("/favicon.ico", http.NotFoundHandler()) // 2
handler := cors.Default().Handler(mux) // 1
http.ListenAndServe(":8080", handler)
}
http.NewServeMux
를 통해 mux를 생성할 수 있다http.ListenAndServe
의 두 번째 파라미터로 넘겨준다favicon.ico
http.NotFoundHandler
를 통해 이 에러를 방지할 수 있다코드를 수정한 후, 다시 위 절차대로 진행해보자.
버튼을 클릭하면 등록 완료라는 알림창이 뜰 것이다. 정말 등록이 잘 되었는지 확인하기 위해 데이터베이스를 다시 조회해본다.
이번에는 반대로 DB로부터 데이터를 가져와서 Form에 띄우는 작업을 진행해 볼 것이다.
<main.go>
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/get", get) // 1
mux.HandleFunc("/api/insert", insert)
mux.Handle("/favicon.ico", http.NotFoundHandler())
handler := cors.Default().Handler(mux)
http.ListenAndServe(":8080", handler)
}
...
type Data struct { // 2
Index string
Title string
Content string
}
...
func get(w http.ResponseWriter, req *http.Request) {
datas := []*Data{}
w.Header().Set("Content-Type", "text/html; charset=utf-8") // 3
db, err = sql.Open("mysql", "root:0000@tcp(127.0.0.1:3306)/simpleboard")
if err != nil {
log.Fatalln(err)
}
conn, err := db.Query("SELECT * FROM simpleboard;")
if err != nil {
log.Fatalln(err)
}
defer conn.Close()
for conn.Next() { // 4
var data Data
conn.Scan(&data.Index, &data.Title, &data.Content) // 5
datas = append(datas, &data)
}
json.NewEncoder(w).Encode(datas) // 6
defer db.Close()
}
Axios.get('http://localhost:8080/api/get')
으로부터 받은 요청에 응답하기 위한 함수이다text/html
로 설정해도 된다application/json
으로 설정해야 한다db.Query
를 사용한 경우, 반복문을 통해 한 줄씩 행을 이동하며 작업을 진행해야 한다.Next
메서드를 통해 다음 행이 있는지 확인할 수 있다다음으로, REACT 서버 코드를 수정한다
❗ 원래의 프로젝트와 다른 내용이 있으므로 아래 코드를 꼭 확인해야 한다
<App.js>
import { useEffect, useState } from 'react';
...
const [viewContent, setViewContent] = useState([]); // 1
...
useEffect(()=>{ // 2
Axios.get('http://localhost:8080/api/get').then((response)=>{
setViewContent(response.data)
})
}, [viewContent])
...
return (
...
<div className="App">
<h1>Game Review</h1>
<div className='game-container'>
{viewContent.map(element => // 3
<div style={{ border: '1px solid #333' }}>
<h2>{element.Title}</h2>
<div>
{ReactHtmlParser(element.Content)}
</div>
</div>
)}
</div>
)
this.state
)으로 해야 했다const [<상태 값 저장 변수>, <상태 값 갱신 함수>] =useState(<상태 초기 값>)
key-value
형태를 가진다여기까지 진행하고 프로그램을 실행하면, 프로젝트의 최종 결과물을 확인할 수 있다. 게시판에 데이터를 입력하고 버튼을 클릭하면, DB에 데이터가 저장되고 REACT가 저장된 결과를 서버로부터 불러와 실시간으로 랜더링하여 화면에 띄워준다.