이번에는 Sprint 풀이를 Server와 Client를 나누지 않고 한번에 써보는게 많은 도움이 될것 같다. 하여 스프린트를 받은 후 모든 파일을 살펴보자.
const express = require('express');
const app = express();
const cors = require('cors');
const PORT = process.env.PORT || 8080;
const handleCallback = require('./controller/callback')
const handleImages = require('./controller/images')
app.use(express.json());
app.use(
cors({
origin: true,
})
);
app.post('/callback', handleCallback);
app.get('/images', handleImages)
app.listen(PORT, () => {
console.log(`listening on port ${PORT}`);
});
module.exports = app;
먼저 Server의 index.js파일이다. 또한 라우터를 직접 받아 연결해주는걸 볼 수 있었다. localhost는 8080 port이며 cors설정으로 origin설정을 ture로 해 주고 있다.
이후 대부분의 파일들은 값이 담겨져 있지않은 파일들이 대부분이다. 흐름에 따라서 파일들을 왔다 갔다 할것이니 어디의 파일을 보고 있는지 잘 확인해야 한다.
먼저, 환경변수의 설정을 해주어야 한다. 우리는 OAuth를 사용하기 위한 수단으로 git Hub를 이용하여 로그인을 구현해 보려고 한다.
https://github.com/settings/developers
위의 사이트로 접속하여 OAuth를 사용하기 위해 해당작업을 수행하여 clientID와 Client Secrets를 받아온다. 위의 값들을 .env에 저장하고 환경변수를 설정해주면 되겠다.
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
위의 파일을 작성을 완료하였다면 이 다음에 controller에 들어있는 파일들이 각각 무슨 역할을 하는지 알아야한다. 테스트를 돌려보고, urclass를 보면 더 자세히 알 수 있다.
현재 우리는 OAuth를 이용하여 로그인 구현을 하려고 한다. 이 방법의 흐름을 간략하게 보자.
위의 흐름대로 1번 부터 따라가면서 만들어보자.
사용자가 클라이언트에 git hub으로 로그인 요청을 보낸다고하면 Client를 먼저 살펴봐야 한다.
import React, { Component } from 'react';
class Login extends Component {
constructor(props) {
super(props)
this.socialLoginHandler = this.socialLoginHandler.bind(this)
// TODO: GitHub로부터 사용자 인증을 위해 GitHub로 이동해야 합니다. 적절한 URL을 입력하세요.
// OAuth 인증이 완료되면 authorization code와 함께 callback url로 리디렉션 합니다.
// 참고: https://docs.github.com/en/free-pro-team@latest/developers/apps/identifying-and-authorizing-users-for-github-apps
this.GITHUB_LOGIN_URL = 'https://github.com/login/oauth/authorize?client_id=a2c04f01082da762eef6'
}
socialLoginHandler() {
window.location.assign(this.GITHUB_LOGIN_URL)
}
render() {
return (
<div className='loginContainer'>
OAuth 2.0으로 소셜 로그인을 구현해보세요.
<img id="logo" alt="logo" src="https://image.flaticon.com/icons/png/512/25/25231.png" />
<button
onClick={this.socialLoginHandler}
className='socialloginBtn'
>
Github으로 로그인
</button>
</div>
);
}
}
export default Login;
리액트로 만들어져 있는 것을 볼 수 있겠다. 버튼을 누르게 되면 socialLoginHandler함수가 실행되면서 git hub로그인이 될 페이지로 넘어가야한다. 이 페이지는 아래의 참조를 살펴보자.
참조
해당 공식 사이트에 들어가보면, 사용자의 github ID요청이 있다. 해당 URL로 요청을 보내면 되는 것이다. 사실 이부분에서는 사용자가 직접 입력하여 해당 ID가 가지고 있는 Client_id를 가지고 로그인이 될 테지만, 현재는 간단한 구현을 위해서 위의 과정을 생략한 것이다.
하여, 내 git hub으로 로그인이 되어야 하기 때문에 URL의 파라미터를 설정해 주어야 한다.
참조
[Javascript] URL 파라미터 값 가져오기 (쿼리스트링 값)
페이지의 URL과 파라미터를 읽기위해 공식문서와 위의 참조를 활용하였다.
'https://github.com/login/oauth/authorize?client_id=a2c04f01082da762eef6'
URL중 쿼리스트링의 내용을 Client_id=로 값을 입력하여 사용자의 Id를 인증하게 되는(?) 것이다.
위의 과정이 끝나게 되면 git hub는 자동적으로 클라이언트로 리디렉션되는데 그 때 파라미터에 Authorization code가 담겨지게 된다. 그 때, 리디렉션이 될 때 아래의 코드로 Authorization Code를 가져올 수 있게 되는 것이다.
componentDidMount() {
const url = new URL(window.location.href)
const authorizationCode = url.searchParams.get('code')
if (authorizationCode) {
// authorization server로부터 클라이언트로 리디렉션된 경우, authorization code가 함께 전달됩니다.
// ex) http://localhost:3000/?code=5e52fb85d6a1ed46a51f
this.getAccessToken(authorizationCode)
}
클라이언트로 리디렉션되었을 경우에 파라미터에서 code를 얻어오는 코드이다. 로그인을 하게 된다면, Callback_url로 설정해둔 곳으로 redirect 되는 것을 볼 수 있을것이다. URI는 아래와 같이 구성된다.
https://example.com/callback/?code=a5e58d098317a84df83a
참조 - authorizationCode
Github Oauth App의 Authorization 처리
참조 - Redirect
JavaScript / 다른 페이지로 리다이렉트(Redirect) 하기
참조 - componentDidMount(useEffect)
code를 잘얻어 오게 되면 이 값을 가지고 서버에게 넘겨주어야 한다.
넘겨주는 방법으로는 axios를 사용해보자.
참조
[AXIOS] 📚 axios 설치 & 특징 & 문법 💯 정리
async getAccessToken(authorizationCode) {
}
위와 같이 getAccessToken이라는 함수의 매개변수로 authorizationCode라는 값이 들어오게 된다. code는 아까도 말했듯 componentDidMount라는 함수에서 들어오게 될 것이다.
axios를 사용하기 위해서 서버가 사용하고 있는 포트를 알아야한다. 앞에서 말했듯, callbcak.js에서 code를 받아 Token을 만드므로 라우터를 /callback으로 설정해주어야 겠다.
async getAccessToken(authorizationCode) {
const result = await axios({
url : "http://localhost:8080/callback",
method : "post",
data :{
authorizationCode
}
})
}
axios의 설정을 위의 코드처럼 해주면 되겠다.
자 이제 그럼 server로 넘어가서 code를 가지고 git hub에 요청을 보내보자.
require('dotenv').config();
const clientID = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
const axios = require('axios');
module.exports = async (req, res) => {
}
파일이 위와 같이 되어 있는 것을 볼 수 있다. 현재 무슨값이 들어오는지, 혹은 code가 제대로 들어오고 있는지 console로 찍어 확인해 보자.
commend
console.log(req.body)
result
{ authorizationCode: 'fake_auth_code' }
위와 같은 결과로 authorizationCode가 정상적으로 들어오는걸 볼 수 있다.
그렇다면 위의 데이터를 토대로 git hub에 post요청을 보내서 AccessToken을 받아오자.
module.exports = async (req, res) => {
console.log(req.body);
// { authorizationCode: 'fake_auth_code' }
const code = req.body.authorizationCode
const result = await axios({
url : 'https://github.com/login/oauth/access_token',
method : 'post',
data : {
client_id : clientID,
client_secret : clientSecret,
code : code
}
})
}
위의 코드로 result가 잘 받아오고 있는지 확인해보자.
commend
console.log(reuslt)
위와 같이 찍어보았는데 엄청난량의 데이터가 쏟아져 나온다. 하여, data 키로 값을 넣었으니 그 값을 가져오자고 하여 다시 찍어보았다.
commend
console.log(reuslt.data)
result
{
access_token: 'fake_access_token',
token_type: 'Bearer',
scope: 'user'
}
데이터가 아주잘 받아져 오는것을 확인할 수 있었다. 그렇다면 이 값을 토대로 AccessToken을 다시 클라이언트로 전해주면 되겠다.
const AccessToken =result.data.access_token
res.status(200).json({accessToken: AccessToken})
require('dotenv').config();
const clientID = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
const axios = require('axios');
module.exports = async (req, res) => {
// req의 body로 authorization code가 들어옵니다. console.log를 통해 서버의 터미널창에서 확인해보세요!
// console.log(req.body);
// { authorizationCode: 'fake_auth_code' }
const code = req.body.authorizationCode
// TODO : 이제 authorization code를 이용해 access token을 발급받기 위한 post 요청을 보냅니다. 다음 링크를 참고하세요.
// https://docs.github.com/en/free-pro-team@latest/developers/apps/identifying-and-authorizing-users-for-github-apps#2-users-are-redirected-back-to-your-site-by-github
const result = await axios({
url : 'https://github.com/login/oauth/access_token',
method : 'post',
data : {
client_id : clientID,
client_secret : clientSecret,
code : code
},
headers : {
accept : 'application/json'
}
})
const AccessToken =result.data.access_token
console.log(AccessToken)
// fake_access_token
res.status(200).json({accessToken: AccessToken})
}
여기서 callback.js는 간단하게 끝이 났다. 다음으로 이제 AccessToken이 만들어졌으니 클라이언트에서 받아야 할것이다. 다시 App.js로 넘어가 AccessToken에 접근해 보자.
import React, { Component } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Login from './components/Login';
import Mypage from './components/Mypage';
import axios from 'axios';
class App extends Component {
constructor() {
super();
this.state = {
isLogin: false,
// TODO:access token이 상태로 존재해야 합니다.
accessToken : null
};
this.getAccessToken = this.getAccessToken.bind(this);
}
async getAccessToken(authorizationCode) {
}
사실 현재 react는 이전에 자주보던 함수형의 모습이 아닌, class형으로 만들어져 있다. 사실 함수형보단 간단한거 같으니 코드를 짜보자.
아까와 비슷하게 이번엔 클라이언트가 서버에게 authorizationCode를 넘겨주면 그 값으로 다시 AccessToken을 넘겨주게 될 것이다.
async getAccessToken(authorizationCode) {
const result = await axios({
url : "http://localhost:8080/callback",
method : "post",
data :{
authorizationCode
}
})
}
위의 result가 잘 받아오고 있는지 확인하기 위해서 console을 찍어보자.
commend
console.log(result)
하지만 이전과 같이 엄청난 데이터가 들어오고 있다. 하여 axios는 data라는 곳에서 데이터를 받아오고 있으니, 다시 console을 찍어보도록 하자.
commend
console.log(result.data)
result
{ accessToken: 'fake_access_token' }
위의 결과와 같이 데이터가 잘 들어오는 것을 볼 수 있겠다. 그렇다면 여기서 이 값에 접근하고, react에 state로 데이터를 관리해주면 되겠다.
const AccessToken = result.data.accessToken
this.setState ({
isLogin : true,
accessToken : AccessToken
})
위와 같은 코드를 입력하면 App.js는 끝났다.
import React, { Component } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Login from './components/Login';
import Mypage from './components/Mypage';
import axios from 'axios';
class App extends Component {
constructor() {
super();
this.state = {
isLogin: false,
// TODO:access token이 상태로 존재해야 합니다.
accessToken : null
};
this.getAccessToken = this.getAccessToken.bind(this);
}
async getAccessToken(authorizationCode) {
// 받아온 authorization code로 다시 OAuth App에 요청해서 access token을 받을 수 있습니다.
// access token은 보안 유지가 필요하기 때문에 클라이언트에서 직접 OAuth App에 요청을 하는 방법은 보안에 취약할 수 있습니다.
// authorization code를 서버로 보내주고 서버에서 access token 요청을 하는 것이 적절합니다.
// TODO: 서버의 /callback 엔드포인트로 authorization code를 보내주고 access token을 받아옵니다.
// access token을 받아온 후
// - 로그인 상태를 true로 변경하고,
// - state에 access token을 저장하세요
// console.log(authorizationCode)
// fake_auth_code
const result = await axios({
url : "http://localhost:8080/callback",
method : "post",
data :{
authorizationCode
}
})
console.log(result.data)
// console.log(result.data.accessToken)
// fake_access_token
const AccessToken = result.data.accessToken
this.setState ({
isLogin : true,
accessToken : AccessToken
})
}
componentDidMount() {
const url = new URL(window.location.href)
const authorizationCode = url.searchParams.get('code')
if (authorizationCode) {
// authorization server로부터 클라이언트로 리디렉션된 경우, authorization code가 함께 전달됩니다.
// ex) http://localhost:3000/?code=5e52fb85d6a1ed46a51f
this.getAccessToken(authorizationCode)
}
}
render() {
const { isLogin, accessToken } = this.state;
return (
<Router>
<div className='App'>
{isLogin ? (
<Mypage accessToken={accessToken} />
) : (
<Login />
)}
</div>
</Router>
);
}
}
export default App;
현재 render부분을 보게되면, props로 accessToken을 Mypage로 내려주고 있는걸 볼 수 있겠다.
다음으로 Mypage를 살펴보기전 어디까지 구현이 되었는지를 살펴보자.
위의 코드를 볼때 현재 5번까지 완료된 것으로 볼 수 있겠다. 이제 얼마안남았다.
MyPage는 AccessToken을 가지고 git hub에 데이터를 요청하여 페이지를 만들면 된다.
import React, { Component } from "react";
import axios from 'axios';
const FILL_ME_IN = 'FILL_ME_IN'
class Mypage extends Component {
constructor(props) {
super(props);
this.state = {
images: [],
// TODO: GitHub API 를 통해서 받아올 수 있는 정보들 중에서
// 이름, login 아이디, repository 주소, public repositoty 개수를 포함한 다양한 정보들을 담아주세요.
}
}
async getGitHubUserInfo() {
}
async getImages() {
}
componentDidMount() {
this.getGitHubUserInfo()
this.getImages()
}
render() {
const { accessToken } = this.props
if (!accessToken) {
return <div>로그인이 필요합니다</div>
}
return (
<div>
<div className='mypageContainer'>
<h3>Mypage</h3>
<hr />
<div>안녕하세요. <span className="name" id="name">{FILL_ME_IN}</span>님! GitHub 로그인이 완료되었습니다.</div>
<div>
<div className="item">
나의 로그인 아이디:
<span id="login">{FILL_ME_IN}</span>
</div>
<div className="item">
나의 GitHub 주소:
<span id="html_url">{FILL_ME_IN}</span>
</div>
<div className="item">
나의 public 레포지토리 개수:
<span id="public_repos">{FILL_ME_IN}</span>개
</div>
</div>
<div id="images">
{/* 이미지를 넣으세요 */}
</div>
</div>
</div >
);
}
}
export default Mypage;
맨처음 해야할 것은 Mypage를 구성할 수 있는 user에 대한 info를 가져오는 것이다.
getGitHubUserInfo()에서 axios로 받아오면 되겠다. 이번엔 서버가 아닌 git에서 직접 받아와야 한다.
그렇다면 이번엔 axios에 들어가는 URL을 어떻게 작성해주어야 할까?
참조
위의 참조를 어떻게 찾나 싶지만, 공식문서를 잘 찾아보는 습관을 가져보자.
const userInfo = await axios({
url : ' https://api.github.com/user',
method : 'GET',
headers : {
authorization : `token ${this.props.accessToken}`
}
})
위의 코드의 결과를 다시 console을 찍어보자.
사실 이 부분에서 아래와 같은 실수를 하였다.
data : {
authorization : {this.props.accessToken}
}
계속 테스트를 돌려봐도 값이 안나오길래 test.js파일을 뒤져보니 아래와 같이 써져 있었다.
authorization : `token ${this.props.accessToken}`
이게 왜 이렇게 들어가는지 궁금하여 공식문서를 다시 찾아보았다.
참조
Identifying and authorizing users for GitHub Apps - GitHub Docs
사용자의 액세스 토큰을 사용하면 GitHub 앱이 사용자를 대신하여 API에 요청할 수 있습니다.
Authorization: token OAUTH-TOKEN
GET https://api.github.com/user
Authorization 헤더를 다음과 같이 설정할 수 있습니다.
"Authorization: token OAUTH-TOKEN" https://api.github.com/user
위와 같은 예제가 친절히 적혀있었다. 근데 사실 너무 찾아보기 힘들다 진짜루 😢
이후 위의 결과가 잘 들어갔는지 확인해보자.
commend
console.log(userInfo.data)
axios는 데이터를 data에 담아 전달한다고 생각하자. 하여 위와 같이 console을 찍었다.
result
{
name: '김코딩',
login: 'kimcoding',
html_url: 'https://github.com/kimcoding',
public_repos: 2
}
위와 같이 데이터를 잘 받아오는걸 볼 수 있다. 위의 데이터를 state로 관리해주자.
let {name, login, html_url, public_repos} = userInfo.data
this.setState({
name : name,
login : login,
html_url : html_url,
public_repos : public_repos
})
위의 state값을 찍으려면 아래와 같은 코드로 사용하면 안된다.
console.log(name)
아래와 같은 코드로 실행하여야 state로 저장되어 있는 값이 잘 나오는 걸 확인할 수 있다.
console.log(this.state.name)
이제 그럼 이미지를 가져오는 함수인 getImages()를 채워보자.
현재 이미지는 서버에서 가지고 있다. 이번엔 git이 아닌 서버에 axios를 날려 데이터를 가져와보자. route는 /images가 되겠다.
const userImage = await axios({
url : ' http://localhost:8080/images',
method : 'GET',
headers : {
authorization : `token ${this.props.accessToken}`
}
})
여기서도 AccessToken을 이용하여 서버에게 인증을 해야알 것이다.
하여 다시 Server로 돌아가서 해당 요청을 받고 요청에 대한 응답을 보내주는 코드를 짜보자.
const images = require('../resources/resources');
module.exports = (req, res) => {
}
먼저 req로 AccessToken이 잘 들어오는지 확인해보자. 클라이언트에서 headers에 담아 보냈으므로 req.headers로 접근해 보자.
commend
console.log(req.headers)
result
{
host: '127.0.0',
'accept-encoding': 'gzip, deflate',
authorization: 'token fake_access_token',
connection: 'close'
}
위의 결과로 AccessToken이 잘 들어오는걸 확인할 수 있다.
조건을 2가지를 생각할 수 있겠다. 만약AccessToken이 없을 경우와 있을경우로. 두 가지의 경우로 코드를 짜보자.
const images = require('../resources/resources');
module.exports = (req, res) => {
// TODO : Mypage로부터 access token을 제대로 받아온 것이 맞다면, resource server의 images를 클라이언트로 보내주세요.
console.log(req.headers)
const AccessToken = req.headers.authorization
if(!AccessToken){
res.status(403).json({message: 'no permission to access resources'})
}
else{
res.status(200).json({images : images})
}
}
이제 다시 Client - component/Mypage로 돌아가서 userImage를 잘 가지고 오는지 console로 확인해보자.
commend
console.log(userImage.data.images)
result
[
{ file: '1.png', blob: 'data:image/png;base64,iVBORw1111' },
{ file: '2.png', blob: 'data:image/png;base64,iVBORw2222' },
{ file: '3.png', blob: 'data:image/png;base64,iVBORw3333' }
]
잘 받아오고 있는걸 볼 수 있다. userImage를 이제 state로 관리하기 위해서 코드를 짠다.
this.setState({
images: userImage.data.images
})
데이터를 모두 잘 받고 있으니 이제 HTML에 들어가야할 데이터 정보를 뿌려주면 되겠다. class형은 함수형과는 다르게 props로 내려주거나 state를 사용하기 위해서는 render안에서 다시 불러와 return으로 내려주어야한다.
render() {
const { accessToken } = this.props
const {name, login, html_url, public_repos, images} = this.state
if (!accessToken) {
return <div>로그인이 필요합니다</div>
}
return (
<div>
<div className='mypageContainer'>
<h3>Mypage</h3>
<hr />
<div>안녕하세요. <span className="name" id="name">{name}</span>님! GitHub 로그인이 완료되었습니다.</div>
<div>
<div className="item">
나의 로그인 아이디:
<span id="login">{login}</span>
</div>
<div className="item">
나의 GitHub 주소:
<span id="html_url">{html_url}</span>
</div>
<div className="item">
나의 public 레포지토리 개수:
<span id="public_repos">{public_repos}</span>개
</div>
</div>
<div id="images">
{/*이미지를 넣어주세요*/}
</div>
</div>
</div >
);
}
}
export default Mypage;
위와 같이 태그에 정보들을 채워 넣어줄 수 있겠다. 헌데 이미지를 어떻게 넣을지 고민이다. 3개의 이미지를 모두 넣어야하는데 내부에서 for문을 돌려야할까 하다 [{},{},{}]의 형태로 되어 있길래 map을 쓰면 되겠구나 했다.
<div id="images">
{
images.map((image ,idx) =>{
return <img key = {idx} src={image.blob}/>
})
}
</div>
file이 .png라고 되어 있어서 image에 file로 접근하였는데 아무런 결과가 보이지 않아 blob으로 바꾸어 보았더니 이미지가 보이게 되었다. 그리고 사실 key를 생각하지는 못하였는데 key를 만들어주지 않으면 react내부에 아래와 같은 경고가 생기게 된다.
heck the top-level render call using <div>. See https://fb.me/react-warning-keys for more information.
in img
하여 이미지 태그들의 각각의 id값을 지정해주기 위해 map에서 두 번째 인자에는 index의 값이 들어가므로 해당 값을 key로 잡아주어 경고를 없앴다.
이렇게 되면 사실상 스프린트는 모두 끝나게 된다.
import React, { Component } from "react";
import axios from 'axios';
const FILL_ME_IN = 'FILL_ME_IN'
class Mypage extends Component {
constructor(props) {
super(props);
this.state = {
images: [],
// TODO: GitHub API 를 통해서 받아올 수 있는 정보들 중에서
// 이름, login 아이디, repository 주소, public repositoty 개수를 포함한 다양한 정보들을 담아주세요.
}
}
async getGitHubUserInfo() {
// TODO: GitHub API를 통해 사용자 정보를 받아오세요.
// https://docs.github.com/en/free-pro-team@latest/rest/reference/users#get-the-authenticated-user
const userInfo = await axios({
url : ' https://api.github.com/user',
method : 'GET',
headers : {
authorization : `token ${this.props.accessToken}`
}
})
// console.log(userInfo.data)
let {name, login, html_url, public_repos} = userInfo.data
this.setState({
name : name,
login : login,
html_url : html_url,
public_repos : public_repos
})
console.log(this.state.name)
}
async getImages() {
// TODO : 마찬가지로 액세스 토큰을 이용해 local resource server에서 이미지들을 받아와 주세요.
// resource 서버에 GET /images 로 요청하세요.
const userImage = await axios({
url : ' http://localhost:8080/images',
method : 'GET',
headers : {
authorization : `token ${this.props.accessToken}`
}
})
console.log(userImage.data.images)
this.setState({
images: userImage.data.images
})
}
componentDidMount() {
this.getGitHubUserInfo()
this.getImages()
}
render() {
const { accessToken } = this.props
const {name, login, html_url, public_repos, images} = this.state
if (!accessToken) {
return <div>로그인이 필요합니다</div>
}
return (
<div>
<div className='mypageContainer'>
<h3>Mypage</h3>
<hr />
<div>안녕하세요. <span className="name" id="name">{name}</span>님! GitHub 로그인이 완료되었습니다.</div>
<div>
<div className="item">
나의 로그인 아이디:
<span id="login">{login}</span>
</div>
<div className="item">
나의 GitHub 주소:
<span id="html_url">{html_url}</span>
</div>
<div className="item">
나의 public 레포지토리 개수:
<span id="public_repos">{public_repos}</span>개
</div>
</div>
<div id="images">
{
images.map((image ,idx) =>{
return <img key = {idx} src={image.blob}/>
})
}
</div>
</div>
</div >
);
}
}
export default Mypage;
App.js
componentDidMount() {
const url = new URL(window.location.href)
const authorizationCode = url.searchParams.get('code')
if (authorizationCode) {
// authorization server로부터 클라이언트로 리디렉션된 경우, authorization code가 함께 전달됩니다.
// ex) http://localhost:3000/?code=5e52fb85d6a1ed46a51f
this.getAccessToken(authorizationCode)
}
}
componant/Mypage
componentDidMount() {
this.getGitHubUserInfo()
this.getImages()
}
참조
[React] 리액트 - 컴포넌트 생명 주기 (Component Life Cycle)
useEffect 는componentDidMount를 흉내내기 위한 방법으로 사용된다고 한다. 컴포넌트를 생성하고 첫 렌더링이 끝났을 때 호출되는 함수이다.