qcoding

3) React + Teachable machine 프로젝트 - React를 통한 프로젝트 제작 본문

React

3) React + Teachable machine 프로젝트 - React를 통한 프로젝트 제작

Qcoding 2022. 3. 20. 13:36
반응형

* 해당 글에서는 전에 만들었던 Teachable Machine을 이용한 모델을 통해 React 프로젝트를 생성하고 완성하는 과정을 포함하고 있다.

 

1) 사용한 라이브러리

-> Create React App을 통해 react 프로젝트를 생성하였다.

npx create-react-app my-app
cd my-app
npm start

-> 1) Styled-component  --> css 사용을 위해 사용 

    2) @teachablemachine/image --> teachable machine js를 사용하기 위해 사용

    3) react-icons --> 웹사이트에서 icons을 사용하기 위해 사용

    4) react-router-dom --> 사이트 routing을 위해 사용

"dependencies": {
    "@teachablemachine/image": "^0.8.5",
    "@tensorflow/tfjs": "^3.13.0",
    "@testing-library/jest-dom": "^5.16.2",
    "@testing-library/react": "^12.1.3",
    "@testing-library/user-event": "^13.5.0",
    "react": "^17.0.2",
    "react-activity": "^2.1.3",
    "react-dom": "^17.0.2",
    "react-icons": "^4.3.1",
    "react-router-dom": "^6.2.1",
    "styled-components": "^5.3.3",
    "web-vitals": "^2.1.4"
  },

-> 여기서 teachable machine에서 모델을 불러오기 위해서는 아래의 패키지를 설치해야 한다. 그런데 이때 설치가 안되는 문제가 있어서 해당 명령어를 통해 강제로 실행하였다. 정상적으로 동작하는 것을 확인하였다. 

npm i @teachablemachine/image --force

https://www.npmjs.com/package/@teachablemachine/image

 

@teachablemachine/image

A support library for use with mobilenet-based models generated by Teachable Machine (https://g.co/teachablemachine). Latest version: 0.8.5, last published: 7 months ago. Start using @teachablemachine/image in your project by running `npm i @teachablemachi

www.npmjs.com

 

2) 화면 구성

-> 화면은 아래와 같이 5개의 기능을 가지고 있으며, 여기서 총 page는 로딩 / home / Main 로 구성되어 있다. 

-> 해당 글에서는 전체의 코드를 다 소개 하기 보다는 핵심이 되는 기능을 정리하여 소개하고, 전체 코드는 아래의 githup을 통해 확인하면 될 것 같다.

https://github.com/leeminq1/politic_test

 

GitHub - leeminq1/politic_test

Contribute to leeminq1/politic_test development by creating an account on GitHub.

github.com

 

2-1) App.js (라우팅)

-> react-router-dom이 v6로 업그레이드 되면서 많이 바뀌게 되었다. 처음에는 v5 버전의 문법으로 작성하였다가 공식문서를 보면서 아래와 같이 수정하였다. 

-> 여기서보면 로딩 컴포넌트를 2.7초간 불러온 후 Home 컴포넌트로 넘어가게 된다. 

import React,{useState,useEffect} from 'react';
import { Route, Routes,Navigate } from "react-router-dom";
import Home from './pages/Home';
import Main from './pages/Main';
import Loading from './pages/Loading';


function App() {
  const [loading,setLoading]=useState(true);

  useEffect(()=>{
    setTimeout(()=>{
      setLoading(false)
    },2700)
  },[])

  if(loading){
    return(
      <Loading></Loading>
    )
  }


  return (
    <Routes>
      <Route exact path="/" element={<Home></Home>}/>
      <Route path="/main" element={<Main></Main>}/>
      <Route
        path="*"
        element={<Navigate to="/" />}
    />
    </Routes>
  );
}




export default App;

2-2)  Home.js 화면 

-> 로딩이 끝난 후에 home 화면이 들어오게 되고, start 버튼 클릭 시 main 페이지로 이동하게 된다. 정리를 위해 styledComponent로 필요한 스타일을 만들어서 import 하여 사용하였다. 여기 까지는 특별한 기능이 필요없고 다음 main 화면에서 여러 필요한 기능을 수행하게 된다.

import React,{useEffect} from 'react'
import { Link } from "react-router-dom";
import styled from 'styled-components';
import {Container,TopContainer,TopTitle,ImageContainer,TopImage,BgImg,TopStart,
    ImageText,Btn,BottomContainer,BottomMainText,BootomSubText} from "../components/styledComponents"


const ImgTextSub=styled.h1`
  font-size:15px;
  font-weight:800;
  color:#b6b6b6;
  margin-top:-10px;
`;


function Home() {
  return (
    <Container>
      <TopContainer>
          <TopTitle>나와 닮은 정치인은?</TopTitle>
          <TopImage src={require("../assets/loading.png")}></TopImage>
      </TopContainer>
      <>
        <TopStart>START!</TopStart>
        <ImageContainer>
            <BgImg src={require("../assets/someone.png")}></BgImg>
            <>
                <ImageText>정면 얼굴 사진을 선택하십시오</ImageText>
                <ImgTextSub>Please choose a picture of your face.</ImgTextSub>
            </>
        </ImageContainer>
        <Link to="/main">
                <Btn>시작하기</Btn>
        </Link>
      </>
      <BottomContainer>
          <BottomMainText>얼굴인식 기술을 활용하여 나와 닮은 꼴 정치인을 찾아드립니다.</BottomMainText>
          <BootomSubText>We'll use facial recognition technology to find a politician who looks like me.</BootomSubText>
      </BottomContainer>
      <div className='adfit'></div>
    </Container>
  )
}

export default Home

2-3)  Main.js 화면

-> 해당 코드에서는 본 사이트의 핵심 기능을 수행하게 된다 .

1) 사용자로 부터 사진 받아오기

2) 받아온 사진을 분석하기 위해 teachable machine의 모델 불러와서 예측하기

3) 예측한 결과를 보여 주기 위하여, 미리 저장된 사진과 , 글을 불러와서 표시하기

4) 공유를 통해 카카오톡 메시지 기능을 사용하여 카카오톡 공유하기 템플릿 사용하기

위와 같이 크게 4가지의 기능을 수행하게 된다. 각각을 확인해 보도록 하자.

 

1) 사용자로 부터 사진 받아오기

-> react에서 사진을 받아올 때 input 태그를 사용한다. 이 때 input 태그를 사용하게 되면 디자인에 제약이 생기므로, div 태그를 통해서 input을 감싸고 div클릭 시 userRef를 통해서 input태그를 클릭하는 효과를 사용하였다. 그리고 input 태그는 display:none 설정을 통해서 보이지 않게 하였다.

-> 사용자가 클릭해서 사진을 업로드하면, setImgBase64의 useState를 통해서 imgbase64에 이미지 주소가 들어가게 되고, 이를 <Image src={imgBase64} /> 로 불러와서 이미지를 보여주게 된다. 

// styled 설정
const ImageUploadContainer=styled.input`
    width:100%;
    height:100%;
    position:absolute;
    top:0;
    display:none;
`;

const ImageContainer=styled.div`
    position:relative;
    width: 70%;
    height: 28%;
    display:flex;
    background-color:rgba(0, 0, 0, 0.07);
    border-radius:10px;
    justify-content:center;
    align-items:center;
    box-shadow: 0px 0px 25px #576574;
    z-index:5;
    flex-direction:column;
    box-shadow: 0px 3px 20px 10px rgba(0, 0, 0, 0.10);
  `;


// 이미지 state
// 파일 imgBase64로 상태를 변겨하여, image 태그에서 src를 통해서 사용할 수 있게한다.

const [imgBase64, setImgBase64] = useState(""); 

// useRef 설정
const inputRef=useRef();


// input tag를 누를 때 입력받은 사진을 통해imgBase64
const handleChangeFile = (event) => {
      setLoading(true);
      setShowResult(false)
      setPredictionArr(null);
      setResult(null);
  
      let reader = new FileReader();
  
      reader.onloadend = () => {
        // 2. 읽기가 완료되면 아래코드가 실행됩니다.
        const base64 = reader.result;
        if (base64) {
          setImgBase64(base64.toString()); // 파일 base64 상태 업데이트
        }
      }
      if (event.target.files[0]) {
        reader.readAsDataURL(event.target.files[0]); // 1. 파일을 읽어 버퍼에 저장합니다.
        setImgFile(event.target.files[0]); // 파일 상태 업데이트
        init().then(
          console.log("init 모델"),
          predict()
        );
  
      }
    }




// input html을 사용하기 위해 ImageContainer 라는 div 태그를 통해서 감싸고 안에 input html 태그는
display:none을 통해 안보여지게 변경함

// Container div를 클릭 시 ref를 통해 input 태그를 누르는 것과 동일한 효과를 낸다.
{showResult?<TopStart>분석결과는?</TopStart>:<TopStartLoading>{loading?"잠시만 기다려주세요!":"사진을 업로드 해주세요!"}</TopStartLoading>}
<ImageContainer onClick={()=>{
  inputRef.current.click();
}}>
<ImageUploadContainer ref={inputRef} onChange={handleChangeFile} type="file" accept="image/*" />
{imgBase64?<Image id="srcImg" src={imgBase64}></Image>: 
<>
  <BgImg src={require("../assets/someone.png")}></BgImg>
  <ImageText>GIVE ME A YOUR PICTURE!</ImageText>
</>
}

2) 받아온 사진을 분석하기 위해 teachable machine의 모델 불러와서 예측하기

-> 위에서 사진을 업로드하게 되면 handleChangeFile() 함수를 통해서 predict () 함수를 수행하게 된다.

위의 코드에서 handleChangeFile를 보면 init().then()으로 init 함수가 async-await의 비동기를 통해 모델을 불러오게 되면, then()을 통해서 predict 함수를 수행하게 된다.

1) init함수 : model url를 통해서 모델을 가져온다.

2) perdict 함수 : 위에서 업로드한 이미지가 들어있는 id="srcImg" 태그에 있는 이미지를 모델로 보내 예측을 수행하게 된다. 예측을 수행한뒤 probability를 내림차순으로 정렬하여 predictionArr 배열에 넣게 된다. 이 중에 가장 확률이 높은 것은 prediction[0].className 가 되며, 이를 통해 result와 keyword의 상태를 업로드 한다.

// const URL = 'https://teachablemachine.withgoogle.com/models/F1nMeBJwX/';
const URL = 'https://teachablemachine.withgoogle.com/models/KAoZrcPlp/';
const modelURL = URL + 'model.json';
const metadataURL = URL + 'metadata.json';

let model


// Load the image model and setup the webcam
    async function init() {
      model = await tmImage.load(modelURL, metadataURL);
      //총 클래스 수
      let maxPredictions;
      maxPredictions = model.getTotalClasses();
  }
  
    async function predict() {
      // predict can take in an image, video or canvas html element
      model = await tmImage.load(modelURL, metadataURL);
      const tempImage = document.getElementById('srcImg');
      const prediction = await model.predict(tempImage, false);
      prediction.sort((a, b) => parseFloat(b.probability) - parseFloat(a.probability));
      setPredictionArr(prediction)
      setShowResult(true)
      setLoading(false)
      setResult(prediction[0].className)
      switch(prediction[0].className){
        case "김정은":
          setKeyword("내래 북한 백두혈통이야");
          break;
        case "마크롱":
          setKeyword("프랑스 최연소 대통령");
          break;
        case "메르켈":
          setKeyword("게르만 철의 여인");
          break;
        case "문재인":
          setKeyword("대한민국 19대 대통령");
          break;
        case "바이든":
          setKeyword("BUILD BACK BETTER");
          break;
        case "보리스존슨":
          setKeyword("브렉시트를 완수하고 영국의 잠재력을 일깨우자");
          break;
        case "시진핑":
          setKeyword("중화인민공화국의 정치인");
          break;
        case "심상정":
          setKeyword("정의당당당 주4일제");
          break;
        case "아베":
          setKeyword("역대 최장 기간 집권한 일본 총리");
          break;
        case "아웅산수치":
          setKeyword("미얀마 민주화운동의 상징");
          break;
        case "안철수":
          setKeyword("의사, 프로그래머, 벤처 기업 CEO, 대학 교수");
          break;
        case "오바마":
          setKeyword("Yes, We can!");
          break;
        case "윤석열":
          setKeyword("법조인 출신 정치인으로, 前 검찰총장");
          break;
        case "이재명":
          setKeyword("변호사 출신 정치인, 前 성남시장");
          break;
        case "줄리아길라드":
          setKeyword("호주 첫 여성총리");
          break;
        case "트럼프":
          setKeyword("MAKE AMERICA GREAT AGAIN!");
          break;
        case "푸틴":
          setKeyword("러시아 상남자");
          break;
        case "허경영":
          setKeyword("내 눈을 바라봐. 넌 건강해지고");
          break;
        case "홍준표":
          setKeyword("대한민국의 검사 출신 정치인. 現 제21대 국회의원");
          break;
        case "힐러리":
          setKeyword("미국 역사상 최초 여성 대통령 후보");
          break;
        default:
          break;
      }
      console.log("가장높은확률 : ",prediction[0].className)
    }
    
  // id="srcImg" 태그를 가져와서 해당 이미지값으로 예측을 수행함  
  <Image id="srcImg" src={imgBase64}></Image>

3) 예측한 결과를 보여 주기 위하여, 미리 저장된 사진과 , 글을 불러와서 표시하기

-> 아래의 코드는 적절한 state를 통해서 결과가 완료되면 결과값을 보여주는 코드이다. 여기서 보면 확률에 따라 80%,50%,20%에 따라서 메시지를 보여주며, 1/ 2/ 3위 까지의 순위를 보여주게 된다. 더보기를 누르면 wikipedia로 연결하게 되는데, 보통은 

window.open(`https://ko.wikipedia.org/wiki/${predictionArr[0].className}`,'_blank')
를 통해서 https://ko.wikipedia.org/wiki/ 뒤에 정치인의 이름을 입력하면 되는데, 트럼프와 마크롱의 경우는
제대로 동작하기 않아서 2가지의 경우만 if문을 통해서 적절한 사이트 주소를 표시해주었다.
{showResult&&result!==null?
        <>
        <MiddleContainer>
            <FaArrowAltCircleDown size={40} color="#323232"></FaArrowAltCircleDown>
            <ResultContainer>
                <ReulstScore>{showResult?`${(predictionArr[0].probability*100).toFixed(1)}%`:null}</ReulstScore>
                <ReulstName>{showResult?predictionArr[0].className:null}</ReulstName>
            </ResultContainer>
            <KeyText>{keyword}</KeyText>
            <SubText>{predictionArr[0].probability*100>80?"도플갱어 아니신가요?":predictionArr[0].probability*100>50?"형제자매가 확실합니다.":predictionArr[0].probability*100>20?"닮았다는 소리 들어보셨죠?":"3초 닮은꼴"}</SubText>
            <MoreInfoBtn onClick={()=>{
              if(predictionArr[0].className=="트럼프"){
                window.open(`https://ko.wikipedia.org/wiki/도널드_트럼프`,'_blank')
                return null
              }else if(predictionArr[0].className=="마크롱"){
                window.open(`https://ko.wikipedia.org/wiki/에마뉘엘_마크롱`,'_blank')
                return null
              }
              window.open(`https://ko.wikipedia.org/wiki/${predictionArr[0].className}`,'_blank')
            }}>더보기</MoreInfoBtn>
        </MiddleContainer>
        <ImageContainer>
            <Image id="srcImg" src={require(`../assets/${result}.jpg`)}></Image>
        </ImageContainer>
        <div className='adfitTwo'></div>    
        <RestResultRow>
          <RestResultCol>
            <RestResultScore>{showResult?`${(predictionArr[1].probability*100).toFixed(1)}%`:null}</RestResultScore>
            <RestResultRank>2위</RestResultRank>
            <RestResultName>{showResult?predictionArr[1].className:null}</RestResultName>
          </RestResultCol>
          <RestResultCol>
            <RestResultScore>{showResult?`${(predictionArr[2].probability*100).toFixed(1)}%`:null}</RestResultScore>
            <RestResultRank>3위</RestResultRank>
            <RestResultName>{showResult?predictionArr[2].className:null}</RestResultName>
          </RestResultCol>
        </RestResultRow>
        </>
      :null}

4) 공유를 통해 카카오톡 메시지 기능을 사용하여 카카오톡 공유하기 템플릿 사용하기

-> 아래 코드는 구글링을 통해서 있는 것을 수정해서 사용하였다. 여기서 카카오 개발자에 가입하여 어플리케이션을 생성하고 아래의 앱키를 받은 뒤 여기서 JavaScript 키를 kakao.init("여기다가 javasciprt key를 넣는다.");  이 부분에 넣는다.

--> 추가로 배포하고 나서는 사이트 url 주소를 사용하면 되지만, 개발 중에는 localhost를 사용하므로, localhost 상태에서도 해당 기능을 사용하려면 아래와 같이 플랫폼에 들어가서 사이트 도메인에 localhost를 추가해주어야 제대로 동작하는 것을 확인 가능하다.

import KakaoShareBtn from '../components/KakaoSharedBtn';

// 카카오톡 공유하기 컴포넌트를 불러와서 사용하고, proprs를 통해 name 이름으로 보내준다.
<KakaoShareBtn name={predictionArr[0].className}></KakaoShareBtn>

--> 위에서 name의 props를 받아 온다음에 그 결과에 맞게 사진과 이름을 가지고 템플릿을 만들었다. 

import React, { useEffect } from "react";
import styled from "styled-components";
import { RiKakaoTalkFill } from "react-icons/ri";

const KakaoShareButton = styled.button`
   background-color:#FED16E;
   padding:10px 15px;
   border-radius:10px;
   color:#353535;
   font-size:15px;
   font-weight:bolder;
   border-width:0px;
   display:flex;
   justify-content:space-evenly;
   align-items:center;
   flex-direction:row;
   @media (min-width: 800px) {
    font-size:20px;
    padding:15px 20px;
  }
`;

const KakaoShareBtn = ({name}) => {

    useEffect(() => {
    createKakaoButton({name});
  }, [name]);


  const createKakaoButton = ({name}) => {
    // kakao sdk script이 정상적으로 불러와졌으면 window.Kakao로 접근이 가능합니다
    if (window.Kakao) {
      const kakao = window.Kakao;
      // 중복 initialization 방지
      if (!kakao.isInitialized()) {
        // 두번째 step 에서 가져온 javascript key 를 이용하여 initialize
        kakao.init("여기다가 javasciprt key를 넣는다.");
      }
      kakao.Link.createDefaultButton({
        // Render 부분 id=kakao-link-btn 을 찾아 그부분에 렌더링을 합니다
        container: "#kakao-link-btn",
        objectType: "feed",
        content: {
          title: "나와 닮은 정치인은?",
          description: `나와 닮은 정치인은 ${name}이네요! 결과를 확인하고 공유해보세요!`,
          imageUrl: `https://politictest-8f628.firebaseapp.com/result/${name}.jpg`,
          link: {
            mobileWebUrl: "https://politictest-8f628.firebaseapp.com/",
            webUrl: "https://politictest-8f628.firebaseapp.com/",
          },
        },
        buttons: [
          {
            title: "웹으로 보기",
            link: {
                mobileWebUrl: "https://politictest-8f628.firebaseapp.com/",
                webUrl: "https://politictest-8f628.firebaseapp.com/",
              },
          },
        ],
      });
    }
  };
  return (
    <KakaoShareButton id="kakao-link-btn">
        공유하기
        <RiKakaoTalkFill size={30} color="black" style={{marginLeft:5}}></RiKakaoTalkFill>
    </KakaoShareButton>
  );
};


export default KakaoShareBtn;

# 위의 카카오템플릿을 사용한 뒤 공유하기를 수행하면 아래와 같이 메시지를 보낼 수 있다.

 

 

반응형
Comments