#28 [서버 프로그래밍] (06.14)

sookyoung.k·2023년 6월 14일
0
post-thumbnail

🐨 솔리디티

* 솔리디티는 절대 복잡하게 코드를 짜지 않는다!
→ 행동이 많고 데이터가 많아지면 가스비가 증가... 웬만하면 솔리디티 안의 내용들은 최대한 간결하게 짜되 조건을 걸어준다 (require구문을 통해서 조건식을 걸어주고 참일 때 코드 실행, 거짓이면 거부 메시지 보낸다)

📍 변수

가시성 지정자 > internal 아니면 public 키워드를 사용하는 것이 가장 일반적 (internal을 제일 많이 사용하신다고 했던 것 같음 public은 너무 다 보여줌)

📍 구조체

구조체란?
→ 여러가지의 데이터를 담을 수 있는 구조를 구상한 것 (틀만 만들어 둔 것)
sql로 치면 테이블(표)을 하나 만들어 둔 거라고 생각하면 된다

* 구조체를 왜 선언하고 어디에 쓰는 걸까?

✔️ 여러 가지의 데이터를 하나의 키 값에 집어넣기 위해서!
✔️ 배열이나 매핑 형태로 데이터를 만들 수 있다
단점
→ 해당하는 구조체를 전체를 한 번에 리턴하는 방법이 없다 ㅠ (배열은 리턴이 되는데 구조체는 리턴이 불가)
아직은 이게 무슨 말인지 잘 모르겠다... 암튼 그렇다니 그냥 그런줄 알고 머리에 집어넣기...오늘 뇌용량 과부화와서 더 못알아보겠음

📍 생성자 함수

deploy시 최초의 한 번만 실행됨 (만들어질 때 최초로 실행되는 함수)

  • 변수에 데이터를 집어넣는 부분에 많이 사용한다

✔️ msg.sender - 처음 deploy가 될 때 deploy를 하는 사람의 지갑 주소를 뜻한다
owner = msg.sender; - 컨트랙트의 주인 등록!
count = 0; - 초기화!

자바 생성자 생성하면 될 것 같기도?

📍 mapping 데이터 생성

* mapping (string => user_info) internal users;
1. key값은 string 타입, usert_info가 value (구조체와 매핑 데이터 연결한 것임)
2. 가시성 지정자
3. users는 변수명임

선언 방식: 매핑 데이터의 1. 타입을 지정하고 2.가시성 보여주고 어떠한 3.이름(변수명)으로 쓸 것인지 (이것도 변수임) 선언

📍 배열 데이터 생성

데이터의 타입을 먼저 지정해주어야 한다 - string[] = '문자열로 이루어진 배열이다'라는 선언

📍 구조체를 이용하여 배열 생성

user_info[] public user_list2;
그냥 지금은 방법을 알려주는 것임! 나중에 쓸 때가 오겠죠

📍 modifier

얘도 함수임 (무언가 행동을 하겠다는 거임)

* 그럼 왜 일반적인 함수와 modifier를 나눠놨을까?

modifier는 함수의 행동을 변경하는 것(modify라는 말 그대로 '수정')

→ 일반적인 함수, modifier를 만들어주고!
함수를 선언한 다음에 그 뒤에 modifier를 같이 넣어주는 방식!

➡️ add_user() 함수를 선언하고 가시성 지정자 뒤에 위에 만들어둔 onlyOwner modifier을 넣어준다!
➡️ onlyOwner 얘를 보면 자체 코드 아래에 _;가 있음. 여기가 add_user() 메소드의 코드가 들어갈 자리라는 뜻임.
➡️

   function add_user(
        string memory _id, // 키값
        string memory _pass,
        string memory _name,
        uint8 _age
    ) public onlyOwner {
        require(msg.sender == owner, "Only owner can call function");
        users[_id].password = _pass;
        users[_id].name = _name;
        users[_id].age = _age;
    }

이렇게 해석하면 됨! 일반 함수에 modifier를 합쳐준 거임!

💡 데이터 구조 알아보기

// mapping data
// key : value 
// mapping (string => user_info)
/*
    {
        key1 (stirng) : {
                password : string, 
                name : string,
                age : uint
        }, ....
    }
*/
// 구조체[]
/*
    [
        {
            password : string, 
            name : string, 
            age : uint
        }, ....
    ]
*/
// json 형태 데이터
/*
data = {
    '1': {
        'password' : '1234',
        'name' : 'test',
        'age' : 20
    }
}
'age'를 변경하려면?
data['1']['age'] = 30

data = {
    '1': 'test1'
}
data에 새로은 '2' 키값과 'test2'라는 value를 추가하려면? 
data['2'] = 'test2' 
data['1'] = 'tset3'
*/

📍 유저의 정보 출력하는 함수 만들기

// 유저의 정보를 출력하는 함수 
    function view_user(
        string memory _id
    ) public view returns (
        string memory, 
        string memory,
        uint8
    ) {
        return (
            users[_id].password,
            users[_id].name,
            users[_id].age
        );
    }

📍 DEPLOY

Remix에서 deploy를 해준다!
그러면 Deployed Contracts에 컨트랙트가 뜹니다.

public으로 지정된 변수까지 다 보여준다!

✔️ add_user에 값을 넣어서 transact 한 다음에 view_user을 확인해보자

짠! value 값 잘 보여줌

⭐ 없는 아이디 값을 call 하면 안 뜸

🐨 Ganache로 스마트 컨트랙트 배포하기

📍 솔리디티 파일 컴파일 및 deploy

사용할 폴더로 이동해서 명령어 npm init -ypackage.json파일을 만들어준다.

* -y는... 'yes'라는 의미임 ㅋㅋㅋㅋ 네네 다 돼요

그리고 우린 ganache로 개발을 할 거니까 truffle init도 해줘야 하는데... 먼저 npm install truffle -g로 모듈(?)을 깔아줘야 한다.


응 안돼

^^ 개오래걸림

install 후 다시 명령어를 실행하면 디렉토리 구조가 이렇게 생성되어 있는 것을 알 수 있다!

* truffle-config.js

버전을 맞춰줍니다.

성격이 급해서 저기 벌써 컴파일 명령어 적어놨는데... 사실 엔터 치기 전임ㅎ 컴파일 전에 Remix에서 작성해둔 스마트컨트랙트를 contracts 폴더에 생성해준다(test.sol 파일 생성 후 복붙 ㄱㄱ)

✔️ compile

그리고 컴파일! 명령어: truffle compile
이전까지는 Remix로만 컴파일을 했던 거고 드디어 다른거로 컴파일을 해본 것임...!

* 캡쳐본은 vscode에 solidity 확장자 설치 안해서 색을 잃은 상태임... 확장자 설치해주기! (그냥 익스텐션에 solidity 검색하면 나온다)

✔️ deploy

deploy를 해보자!

먼저 migrations 폴더에 01_deploy.js 파일 생성

* migrations는 스마트 컨트랙트를 배포 및 관리하는 폴더!
→ 계약을 배포하고 관리하는 일련의 과정을 프로그래밍 언어 중 하나이 JS로 기술한 것

const test = artifacts.require('./Test');

module.exports = function (deployer) {
   deployer.deploy(test).then(function () {
      console.log(test);
   });
};

node.js
✔️ module.exports: 모듈을 해당 스코프 밖으로 보낼 때 사용
module: 현재 모듈에 대한 정보를 갖고 있는 객체 (예약어), exports 객체를 가지고 있다
module.exports는 하나의 변수나 함수 또는 객체를 직접 할당 (이렇게 할당한 객체 안에 넣어둔 변수나 함수를 메인 파일에서도 사용 가능)
* require: 다른 모듈 사용할 때 사용

* 참고 링크
https://baeharam.netlify.app/posts/javascript/module

스마트 컨트랙트
✔️ artifacts.require();: 계약 정보(artifact)를 읽어오는 메소드
✔️ deployer.deploy(test): 가져온 계약 정보를 배포하는 코드
deployer: truffle이 제공하는 배포를 위한 툴
deployer.deploy(): deployer이 제공하는 deploy() 함수 호출
➡️ 위에서 읽어온 계약 정보인 test를 넘겨준다~

* 참고 링크
https://ko.docs.klaytn.foundation/content/getting-started/quick-start/deploy-a-smart-contract
https://medium.com/dnext-post/solidity-tutorial-6-543bd342d928

deploy 명령어가 따로 있긴 하지만

truffle migrations, truffle migrate: compile과 deploy를 동시에! → 가나슈에 배포가 자동으로 됩니다

ganache에 가보면 블록이 생성된 걸 확인할 수 있다! deploy 성공~


build\contracts 폴더 속 Test.json파일을 확인해보면,

network에 주소값 생성된 것도 확인할 수 다!

여기까지가 truffle을 이용하여 컴파일을 해본 것입니다! (remix말고 다른 방법을 알게 되었네용~)

🐨 스마트컨트랙트와 웹 서버 연결

📍 개발에 필요한 패키지 설치
➡️ npm install express ejs web3

* 용어 정리

  • nodeJS 프레임워크 - 자바스크립트를 웹 서버에서 사용할 수 있게 해준다
  • npm - nodeJS에서 여러 패키지를 설치하고 관리할 때 사용되는 패키지 매니저
  • npm init -y 명령어 실행 → 프로젝트 폴더 내에 package.json 생성되고 npm을 사용해서 패키지를 설치할 때마다 dependency 부분에 설치한 패키지와 버전이 입력된다.
  • express - 가장 인기 있는 Node 웹 프레임 워크
    → nodeJS에서 웹 애플리케이션 혹은 API 서버를 구축하는데 가장 많이 사용되는 대표적인 프레임워크다. (빠르고 개방적인 웹 프레임 워크)
    npm install express → 프로젝트 폴더에 node_modules 폴더가 생성되고 자동으로 express 패키지가 참조하고 있는 다른 npm 패키지들이 설치된다.
    https://developer.mozilla.org/ko/docs/Learn/Server-side/Express_Nodejs/Introduction
  • ejs 템플릿 엔진 - 템플릿 엔진은 템플릿을 읽어 엔진의 문법과 설정에 따라 파일을 HTML 형식으로 변환시키는 모듈이다!
    → ejs = Embedded Javascript, 자바스크립트가 내장되어 있는 html 파일
    npm install ejs: EJS 템플릿 설치
    https://velog.io/@yunsungyang-omc/Node.js-EJS-%ED%85%9C%ED%94%8C%EB%A6%BF-%EC%97%94%EC%A7%84
  • web3 - JavaScript용 API로 dAPP을 만들기 위한 가장 기본이 되는 API → API를 이용하여 이더리움 네트워크에 접근해서 웹이나 모바일을 개발할 수 있다. (이더리움 노드를 일반 사람들이 알아보기 쉬운 언어로 바꿔서 개발할 수 있게 만들어준다...는 모듈인 것 같다)
    https://velog.io/@kysung95/BlockChain-Web-3-js%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90

📍 app.js 작성
→ node module을 로딩하고 초기화해야 하는 변수나 Object를 선언하고 Router에 유입이 이루어지는 유입점 역할을 하는 JavaScrip

모든 파일들의 중심이 되는 파일

// express 로드
const express = require('express');
const app = express();

const port = 3000;

// view 파일들의 기본 경로 설정
app.set('views', __dirname + '/views');
// view engine 설정
app.set('view engine', 'ejs');

app.listen(port, function () {
   console.log('server start');
});

➡️ require() 메소드를 통해 외부 모듈을 가져온다. express 모듈을 가져왔다. (http 모듈처럼 사용할 수 있지만 훨씬 더 많은 기능이 있음)
➡️ express 모듈이 가진 주요 메소드

  • set(name, value) - 서버 설정을 위한 함수
  • get(name) - 설정된 서버 속성을 꺼내온다
  • use([path], function, [function...]) -미들웨어 함수 사용
  • get([path], function) - 특정 경로로 요청 정보 처리
  • redirect() - 웹 페이지 경로 강제 이동
  • send() - 클라이언트에 응답 데이터를 보낸다

➡️ 미들웨어 - use() 메소드의 매개변수에 입력하는 함수
➡️ listen() 메소드 - http 모듈의 서버 객체가 가진 메소드임... 서버 실행! 그리고 클라이언트를 기다린다.

📍 nodemon app.js - app.js 서버 오픈 (자동으로 restart)

서버를 오픈했으니 간단하게 회원가입/로그인 기능이 있는 페이지를 만들어 볼 예정!

우선 로그인 화면을 만들어준다!

📍 login.ejs 만들기!

<!DOCTYPE html>
<html lang="ko">
   <head>
      <meta charset="UTF-8" />
      <meta http-equiv="X-UA-Compatible" content="IE=edge" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Document</title>
   </head>
   <body>
      <h1>로그인</h1>
      <form action="/signin" method="post">
         <!-- id를 입력하는 공간 -->
         <label>id</label>
         <!-- 웹데이터는 기본적으로 json! name으로 키 값 지정해줘야 함 (json은 키값, 밸류값 필요하다) -->
         <input type="text" name="_id" /> <br />
         <label>password</label>
         <input type="password" name="_pass" /> <br />
         <input type="submit" value="로그인" />
      </form>

      <!-- 이건 get일까 post일까? 일반적으로 데이터를 요청하는 방식은 get! -->
      <a href="/signup">회원 가입</a>
   </body>
</html>

간단하게 로그인 기능을 가진 화면을 만들었다. 데이터를 주고받기 위해서는 form 태그 안에 input 태그를 넣어줬다. 키 값과 매치하기 위해서 name 속성 넣어줬다!

로그인을 하기 전에 회원가입을 해야 함! 그래서 a태그로 회원가입 버튼을 만들었다.

근데 /signup으로 이동할 주소가 없죠?

📍 app.js에다가 signup, signin 주소 만들어주기

// 회원가입 페이지를 보여주는 주소 생성 (여기서는 화살표 함수를 써봤습니다)
app.get('/signup', (req, res) => {
   res.render('signup.ejs');
});

app.post('/signup2', function (req, res) {
   // post 형태에서 데이터가 존재하는 곳은? req.body.(key)
   const input_id = req.body._id;
   const input_pass = req.body._pass;
   const input_name = req.body._name;
   const input_age = req.body._age;

   // 이렇게 데이터가 많으면... 꼭 확인해주기! 
   console.log(input_id, input_pass, input_name, input_age);

   res.send('signup2');
});

➡️ res객체가 render() 함수를 통해 signup.ejs 파일을 띄워준다. (view engine 설정을 해줬기 때문에 .ejs는 안써도 되긴 함)

➡️ 입력받은 데이터를 할당해주고 콘솔로 데이터가 잘 들어오는지 확인해준다. 우선은 send() 함수로 화면 잘 띄워주는 건지 확인만 하려고 함!

그리고 회원가입을 위한 창이 없기 때문에 화면을 또 하나 만들어야 한다!

📍 signup.ejs 만들어주기~
→ 솔리디티 파일에서 매핑해준 데이터를 받아와야 함

<!DOCTYPE html>
<html lang="ko">
   <head>
      <meta charset="UTF-8" />
      <meta http-equiv="X-UA-Compatible" content="IE=edge" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Document</title>
   </head>
   <body>
      <h1>회원가입</h1>
      <!-- 유저가 입력한 데이터를 서버에게 보내주기 위해 form 생성-->
      <form action="/signup2" method="post">
         <label>id</label>
         <input type="text" name="_id" /><br />
         <label>password</label>
         <input type="password" name="_pass" /><br />
         <label>name</label>
         <input type="text" name="_name" /><br />
         <label>age</label>
         <input type="number" name="_age" /><br />
         <input type="submit" value="회원가입" />
      </form>
   </body>
</html>

화면이 잘 만들어졌으면 잘 작동하는지 확인을 해봅시다!
회원가입 창으로 넘어가면 아래와같은 화면이 뜬다.

인풋창에 데이터 보내줘 봅시다~

잘 보내줬음! 'signup2' 무사히 뜬당~~
→ 화면 바뀌나 확인하려고 res.send('signup2') 응답 메시지를 띄운 것임

콘솔에도 데이터가 잘 들어왔다.

📍 contract 정보가 담긴 json 파일 로드

➡️ require() 함수로 Test.json 파일을 가져온다!
➡️ abi = Application Binary Interface : 스마트 컨트랙트 안에 존재하는 함수와 매개변수들을 json 형식으로 나타낸 리스트이다. abi를 사용해 컨트랙트 내의 함수를 호출하거나 컨트랙트로 부터 데이터를 얻을 수 있다.

→ 이런 식으로 json 형식 데이터들이 저장되어 있음
➡️ const contract_address = Test.json 안의 network 값에서 지갑 주소를 얻어온다.

→ json 파일 구조 잘 확인
➡️ 지갑 주소를 잘 가져오는지 콘솔에 찍어보기

📍 컨트랙트가 배포된 네트워크 등록

해당 네트워크에서 컨트랙트를 찾아가게 됨

➡️ Web3 = 분산형 인터넷
→ 각 사용자가 노드가 되어 탈중앙화된 분산 네트워크 구성
➡️ Web3: 이더리움 네트워크와 상호작용 할 수 있는 다양한 메서드를 제공하는 자바스크립트 라이브러리 (블록체인과 상호작용하는 클라이언트 개발)

  • 다른 계정으로 이더 전송
  • 스마트 컨트랙트에서 데이터를 읽고 쓰기
  • 스마트 컨트랙트 생성
    Web3 Instance
    ➡️ provider어떤 네트워크에 연결할지 설정
    ➡️ providers: web3.js와 연결된 노드 (이더리움 네트워크는 노드로 구성되어 있고, 각 노드는 블록체인의 복사본을 가지고 있다) → provider 클래스를 포함한 객체 리턴 (이 클래스를 사용해 provider 생성 가능
    ➡️setProvider: provider가 다른 하위 모듈이 있는 경우 하위 모듈 별로 provider 설정
    ➡️ HttpProvider: http에서 동작하는 node와 연결하여 web3 객체 생성

* 링크 참고
https://velog.io/@citron03/web3.js%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C
https://velog.io/@devjeenie/%EC%84%B8%EC%83%81%EC%9D%84-%EB%B0%94%EA%BF%80-Web-3.0-Web3.js%EB%9E%80
http://www.umlcert.com/ethereum-dapps-14-2/
https://muyu.tistory.com/entry/Ethereum-web3js-%EC%82%AC%EC%9A%A9%EB%B2%95-%EA%B0%84%EB%8B%A8-%EC%9A%94%EC%95%BD

* 버전 차이 때문에... 안됐음... 고쳐서 쓰세용 (문법이 바뀜)
npm uninstall web3로 버전 삭제 후
npm install web3@1.10.0으로 버전 낮춰서 다시... 하면 됨
아니 버전이 달라지면서 문법이 달라지면 인간적으로 동네방네 알려줘야하는거 아니냐
https://docs.web3js.org/guides/web3_upgrade_guide/x/
web3 불러올때
const { Web3 } = require('web3'); 해주면 된다

📍 배포한 컨트랙트의 주소와 abi를 이용하여 컨트랙트 로드

이렇게 스마트 컨트랙트와 연동 끝!

➡️ eth: 이더리움 블록체인과 컨트랙트와의 상호작용 지원
➡️ Contract(): 컨트랙트 호출!
💡 Web3.js가 스마트 컨트랙트와 통신하기 위해서는 ABI컨트랙트 주소가 필요하다
➡️ abi: 기본적으로 JSON 형태로 컨트랙트의 메소드를 표현하는 것 (어떤 형태로 함수 호출을 해야 컨트랙트가 이해할 수 있는지알려줌)
= 컨트랙트를 배포하고 그 값을 abi코드로 블록체인에 올린다

* 링크 참고
https://monee1001.tistory.com/37
이더리움 블록체인 web3.js 스마트컨트랙트 실행
세상을 바꿀 Web3.0 - 실습

📍 회원가입 (유저 정보 등록)


회원가입을 다시 해보면! 메인페이지로 다시 잘 돌아오고


콘솔에도 잘 찍혀있다.

📍 로그인

✔️ 아이디 패스워드가 일치하는 경우 → 로그인 성공
✔️ 로그인 실패

  1. 아이디가 틀릴 경우 → 에러가 아니라 아예 값이 안 나옴 (데이터가 존재하지 않는다! 아까 리믹스에서 확인한 거)
  2. 비밀번호가 틀릴 경우 → 로그인 실패

실제 문구는 '아이디 혹은 비밀번호가 틀렸다'는 안내를 함
왜 구체적으로 얘기 안 해줄까?
보안 때문에! (해당 아이디가 존재하는지 존재하지 않는지 알아버리기 때문에)

작성 후 로그인을 시켜보면

res.send(result) 값이 화면에 출력된당~~ 완성~~

✔️ 아이디 틀렸을 때

json 데이터에 빈 값으로 나온다

로그인 성공/실패 코드를 이어서 작성해봅시다!

// 로그인 관련 주소를 생성
app.post('/signin', function (req, res) {
   // 유저가 보낸 데이터를 변수에 대입
   const input_id = req.body._id;
   const input_pass = req.body._pass;
   // 값 잘 들어왔는지 확인하는 작업은 꼭 해주세요
   console.log(input_id, input_pass);

   // smartcontract를 이용하여 해당하는 아이디가 존재하는지 체크
   // 데이터가 존재한다면 유저가 입력한 password와 데이터의 password 값을 비교
   // 두 값이 같다면 로그인 성공
   // 그 외의 경우는 로그인 실패
   smartcontract.methods
      .view_user(input_id) // 해당 함수가 요구하는 인자는 하나 (키값)! 얘는 데이터를 리턴하기만 해서(호출만 함) 트랜잭션 x 수수료 발생 x
      .call() // 호출
      .then(function (result) {
         //  res.send(result); // 데이터가 어떻게 들어오는지 확인해봄
         // result는 {'0': password, '1': name, '2': age}
         // 로그인이 성공하는 조건
         // result['0'] == input_pass 그리고 result['0'] != ""
         if ((result['0'] == input_pass) & (result['0'] != '')) {
            res.render('index.ejs');
         } else {
            res.redirect('/');
         }
      });
});

이런 에러가 나오는게 정상이다! index.ejs를 찾을 수 없기 때문이다... 왜냐면 우린 아직 ejs를 만들지 않았음 ㅋㅋㅋㅋ

index.ejs

<!DOCTYPE html>
<html lang="ko">
   <head>
      <meta charset="UTF-8" />
      <meta http-equiv="X-UA-Compatible" content="IE=edge" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Document</title>
      <script>
         function user_list() {
            location.href = '/user_list';
         }
      </script>
   </head>
   <body>
      <h1>Index Page</h1>
      <p>(<%=name%>)님 환영합니다.</p>
      <button onclick="user_list()">회원 리스트 보기</button>
   </body>
</html>

* 화면에 로그인 한 사람의 이름을 띄우고 싶어용
app.js 수정하러 가봅시다

✔️ 함수 기본 원리

// 매개변수가 존재하는 기본값 지정 함수
function func_3(a, b = 3) {
   result = a + b;
   return result;
}

console.log(func_3(3));
// 기본값을 바꾸고 싶다면?
console.log(func_3(3, 5)); // 걍 바꿔줌 응 나 5 넣을거야~ 하는 느낌 (값을 안 집어넣었을 때는 기본값을 주는거고, 값을 집어넣으면 저항없이 바꿔줌)

이런 방식으로

json 값을 , 뒤에 넣어주고 이걸 html위에 띄우기 위해 다시 index.ejs로 고고


태그 안에 %가 있으면 ejs가 '아하 이건 javascript구나!'라고 인지한 후 변수의 값을 이 태그 안에 할당시켜준다
→ 만약 =를 안 쓰면 그냥 자바스크립트 코드를 쓸 수 있음

로그인 성공입니다~!!

📍 가입한 유저 확인 (배열 데이터 사용)

modifier 추가, 배열 데이터 push() 메소드 추가
→ 유저가 얼마나 가입했는지 확인하려는 거임

유저 수 세는 함수 추가

배열 출력할 수 있는 함수 추가
→ 모든 유저의 정보를 가져올 수 있다

📍 아이디 체크 (미리 만들어 둔 것)

📍 재배포!

truffle migrate

테스트 해보기

test1, test2 계정 생성

📍 회원 리스트 보기

index.ejs에 버튼 태그 추가

주소 만들었으니까... app.js로 가야겠죵

➡️ async, await는 따로 글 써서 공부해보자!
막히는 부분 없이 실행시키고 실행시킬 때 쓴 다는 것 같다...?

그리고 user_list.ejs를 만들어준다!

그럼 로그인 후 버튼 눌렀을 때

이렇게 나오면 일단 성공~

📍 배열 안의 데이터 꺼내기

일단 이렇게 나옴... 시간부족으로 우선 여기서 끝

profile
영차영차 😎

0개의 댓글