728x90

React, Vue , Angular 는 보통 프론트엔드 UI 프레임워크로 묶여서 자주 언급됩니다. 사실 처음 개발을 시작할 때, 유저 수가 가장 많은 React를 선택했고 React기반의 RN 이나 Next.js를 사용해봤지만 아직 다른 UI프레임워크를 경험한 적이 없습니다. 그래서 React 다음으로 유저수가 많은 Vue의 공식문서를 통해 학습하고 간단한 TODOLIST를 만들며 느낀점을 공유하고자 합니다.

 

VUE 에 대해 전해 들은 장점

VUE가 쉽다는 얘기를 평소에 많이 들었습니다. 면접을 갔을 때도 생산성에 대한 질문도 많이 받았고, VUE를 쓰는 기업이라면 VUE의 생산성에 대해서 긍정적인 얘기를 합니다.  또한 React와 다른 양방향 바인딩에 대한 얘기도 많이합니다. 

https://www.youtube.com/watch?v=-tVaahsXpwk&list=PLfLgtT94nNq3Br68sEe26jkOqCPK_8UQ-&index=1

 

또한 Vue를 쓰다 React를 쓰는 경우 리렌더링을 잡기 어렵다는 얘기도 많이 들었습니다.

 

하지만 React를 쓰는 입장에서 사실 생산성이란 익숙함에서 더욱 기인하지 않을까?라는 생각이 들었고, 굳이 Vue에 대해 학습할 필요성을 느끼기 보단, React를 더욱 잘 쓰는 방법이 제 관심사였습니다.

하지만 최근 들어 Vue에 대해서 경험하고자 간단하게 Todolist를 작성하면서 학습하게 되었습니다.

 

 

Vue와 React의 비교

1. 진입점

진입점의 경우에는 비슷하다고 느꼈습니다.

// react의 main.tsx

import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
    <App />
)

 

// vue의 main.ts

import { createApp } from 'vue';
import './style.css';
import App from './App.vue';

createApp(App).mount('#app');

 

 함수명이 Root와 app이 다르고 render냐 mount냐 다르지만 결국 함수들이 선언적으로 되어있고, 의도하는 바는 동일해 보입니다.

 

2.  SFC와 JSX(TSX)

Vue의 경우에는 SFC라는 .vue로 파일이 구성되어 있고, React의 경우 컴퍼넌트를 jsx 혹은 tsx에서 사용합니다.

 

//App.vue

<script lang="ts">
import { defineComponent } from 'vue';
import TodoApp from './components/TodoApp.vue';

export default defineComponent({
  components: {
    TodoApp,
  },
});
</script>

<template>
  <div id="app">
    <h1>Todo List</h1>
    <TodoApp />
  </div>
</template>

<style>
#app {
  font-family: Arial, sans-serif;
  text-align: center;
  margin: 20px;
}
</style>

 

// App.tsx
import TodoApp from "./components/TodoApp";

function App() {
  return (
    <div>
      <h1>Todo List</h1>
      <TodoApp />
    </div>
  );
}

 

SFC는 Single-File Components 의 약자로  html, css, js를 하나의 컴퍼넌트에서 관리하는 Vue의 파일형식입니다.

template에서 html , script 에서 js , style에서 css를 다룹니다. 물론 별도의 module을 import하는 방식으로 css와 js를 관심사 분리할 수도 있습니다. 하지만  Vue측의 입장은 "관심사항의 분리가 파일 유형의 분리와 동일한 것이 아니다는 관점으로 바라보는 것이 중요하다"입니다. 하나의  컴퍼넌트가 명확하게 관심사가 분리가 되어있다면 , 굳이 css나 js를 따로 분리하기보다 응집되는 것 또한 매력적일 수 있기 때문입니다.

이때 style의 경우에는  scoped 태그를  적용함으로서 module.css 역할을 할 수 있게 할 수 있습니다.

 

3. 상태 관리

vue의 경우에는 v-model을 통한 양방향 바인딩을 지원합니다.

아래는 vue로 만든 간단한 todolist 입니다.

<script lang="ts">
import { defineComponent, ref } from 'vue';

// Todo 타입 정의
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

export default defineComponent({
  name: 'TodoApp',
  setup() {
    // 상태 정의
    const newTodo = ref<string>(''); // 입력 필드 상태
    const todos = ref<Todo[]>([]); // 투두리스트 배열

    // 할 일 추가
    const addTodo = () => {
      if (newTodo.value.trim() === '') return;
      todos.value.push({
        id: Date.now(), // 고유 ID 생성
        text: newTodo.value.trim(),
        completed: false,
      });
      newTodo.value = '';
    };

    // 할 일 삭제
    const removeTodo = (index: number) => {
      todos.value.splice(index, 1);
    };

    // 할 일 완료/미완료 토글
    const toggleComplete = (index: number) => {
      todos.value[index].completed = !todos.value[index].completed;
    };

    return {
      newTodo,
      todos,
      addTodo,
      removeTodo,
      toggleComplete,
    };
  },
});
</script>

<template>
  <div>
    <div class="input-container">
      <input
        type="text"
        v-model="newTodo"
        placeholder="할 일을 입력하세요"
        @keyup.enter="addTodo"
      />
      <button @click="addTodo">추가</button>
    </div>
    <ul>
      <li
        v-for="(todo, index) in todos"
        :key="todo.id"
        :class="{ completed: todo.completed }"
      >
        <span @click="toggleComplete(index)">{{ todo.text }}</span>
        <button @click="removeTodo(index)">삭제</button>
      </li>
    </ul>
  </div>
</template>

 

vue의 경우에는 ref나 reactive등을 활용하여 상태관리를 할 수 있습니다. 

이때 v-model을 활용한 양방향 바인딩을 통해서 손쉽게 이벤트 핸들러 및 상태관리를 할 수 있습니다.

 

 

Vue를 사용하며 느낀점

vue를 사용하며 느낀점은 만약 바닐라 자바스크립트까지만 사용한 시점에서 새로운 프레임워크를 배운다면 vue가 react보다 사용이 쉽다고 느껴졌습니다. 컴퍼넌트의 경우에 매번 리랜더링되는 react와 다르게 Vue의 SFC파일은 변경된 상태에 대해서만 감지하여 최적화 측면에서도 수월한 점이 존재하다고 느꼈습니다. 

 

하지만 사용 유저가 많다는 커뮤니티적인 장점도 존재하고, 익숙함이 존재하는 시점에서 제게는 React가 더 쉽게 느껴졌습니다. Vue만의 독특한 문법들도 많이 존재하였습니다. 새로 배우는 입장에서 외울게 많다는 게 비용으로 느껴졌고, 좀 더 javascript 답게 코드를 작성할 수 있는 것은 React라는 느낌을 받았습니다. 

또한 오픈소스의 양적 측면에서도 React가 유리하다고 느꼈습니다.

 

다만 React와 Vue의 공식문서를 읽으면서 css의 중첩을 막기 위해 module이나 css-in-js를 사용하여 지역단위로  css를 적용하는 방식이나  사이드 이펙트를 감지하기 위해서 watcher 혹은 useEffect를 사용하는 방식이 결국 프레임워크는 도구나 사용법의 차이지 도달하는 방향은 유사하다는 느낌을 받았습니다.

 

 

 

 

 

728x90

부모님이 컴퓨터에 익숙하지 않다보니, 대신 예약을 해드리는 일이 종종 있습니다.  골프 예약 같은 경우에 경쟁이 치열하다 보니 종종 실패하더군요... 그래서 관련해서 도움을 받을 수 있는 예약 도우미 확장자를 만들었습니다.

 

그 과정에 보편적으로 쓸만한 기능들 그리고 트러블 슈팅 내역을 공유하고자 하여 작성하게 되었습니다.

 

제가 생각하는 기능은

1. 단축키를 통해서 원하는 시간에 예약하기

2. 그리고 해당 페이지에서 예약 과정 처리하기입니다.

 

이 과정에서 필요하다 느낀 기능은 알람, 페이지 접근, 스크립트 사용, 저장소 사용입니다. 

(확장 프로그램의 경우에 결국 브라우저가 아니기 떄문에 웹api에서 제공하는 alert대신 를 chrome api에서 notification을 사용해야 하더군요...) 돔 요소를 제어하는 거는 기존 프론트엔드에서 하던 js를 사용하면 되지만 다른 기능들을 구현하기 위해선 추가적인 확장 프로그램에서 어떤 기능을 제공하는지를 알 필요가 있었습니다.

 

우선 공식 문서를 통해서 학습하려고 했습니다. 개인적으로 구글측 공식문서의 경우에 하나의 완성된 앱이 아닌, 부분적인 코드 위주로 보여줘서 처음부터 보면 어렵게 느껴지더군요..


그래서 블로그를 서칭해서 완성된 예시 2개를 참고했습니다.

https://yscho03.tistory.com/103

 

크롬 확장 프로그램(Chrome Extension)을 개발해보자

크롬 확장 프로그램 (Chrome Extension) 소개 Chrome Extension 이란? Chrome 브라우저의 작은 소프트웨어 프로그램이다. 활용분야 생산성 도구 웹 페이지 콘텐츠 보강 정보 집계 게임 동작방식 웹과 동일하

yscho03.tistory.com

 

https://m2kdevelopments.medium.com/19-understanding-chrome-extensions-commands-3bfa5ecd62cd

 

19 Understanding Chrome Extensions Commands

The chrome.commands API is used to enable the chrome extension run actions from keyboard shortcuts.

m2kdevelopments.medium.com

 

첫 번째 게시글은 한글로 되어있다보니, 어떻게 확장 프로그램 앱이 구성되어있는지, 그리고 등록은 어떻게 하는지에 대해서 파악하기 좋았습니다. 

그리고 두 번쨰 게시글에서는 command 입력에 대해서 전체적인 흐름을 잡기 좋았습니다.

이후 필요한 기능에 대해서 공식 문서를 확인하며 적용시켰습니다.

이제 학습 과정에 대해서 공유드렸고, 어떻게 구현했는지에 대해서 설명하고자 합니다.

 

확장프로그램 개발하기

완벽한 코드를 공유하기보다는 어떤 식으로 기능을 구현했는지를 설명하고자 합니다.


manifest.json

manifest.json은 확장 프로그램의 진입점 역할을 합니다. 어떤 기능을 하는지, 어떤 권한을 하는지를 작성할 수 있습니다.

이제 어떤 키값을 설정했고, 그에 대한 설명을 이어나가려고 합니다.

확장 프로그램을 만들려면 최소한의 키가 필요한데 이는 name, description , version ,manifest_version, icons입니다.
각각 확장 프로그램의 이름, 설명, 프로그램 버전 관리, 사용하고 있는 manifest의 버전, icon입니다.

이 중 manifest버전은 크롬에서 제공하고 있는 버전을 표시하는 것으로 가장 최신인 버전이 3입니다.

 

모든 키 값을 사용하진 않고, 설명하기엔 방대하기에 관련 내용은 상단 manifest.json링크를 통해 확인하면 됩니다.

저는 제가 사용한 키 값에 대해서만 우선 설명하려고 합니다.

// manifest.json

{
  "name": "예약 도우미",
  "description": "~~~ 설명.",
  "version": "0.01",
  "manifest_version": 3,
  "permissions": ["storage", "notifications", "scripting"],
  "host_permissions": ["https://특정사이트.co.kr/*"],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "/images/icons-16.png",
      "32": "/images/icons-32.png",
      "48": "/images/icons-48.png",
      "128": "/images/icons-128.png"
    }
  },
  "commands": {
    "_execute_action": {
      "suggested_key": {
        "default": "Ctrl+Shift+Y"
      },
      "description": "Execute action when the extension icon is clicked"
    },
    "start_reservation": {
      "suggested_key": {
        "default": "Alt+T"
      },
      "description": "예약 시작"
    },
    "fill_cert_no": {
      "suggested_key": {
        "default": "Alt+U"
      },
      "description": "인증 번호 입력"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches":["https://특정사이트.co.kr/*"],
      "js": ["content.js"]
    }
  ],
  "icons": {
    "16": "/images/icons-16.png",
    "32": "/images/icons-32.png",
    "48": "/images/icons-48.png",
    "128": "/images/icons-128.png"
  }
}

 

permission :확장 프로그램이 특정 권한을 사용할 때 유저에게 안내를 하기 위해 사용합니다. 권한이 등록되지 않은 기능은 작동하지 않으므로 내가 어떤 기능을 구현하고 그러기 위해 필요한 권한에 대해서 확인할 필요가 있습니다.

제가 사용한 권한은 아래 3가지입니다.

  • storage: 브라우저의 저장소(local storage)에 데이터를 저장하거나 읽는 권한.
  • notifications: 알림(notification)을 생성하는 권한.
  • scripting: 실행 중인 탭에 스크립트를 주입하는 권한.

host_permissions :  확장 프로그램이 작동할 도메인을 지정합니다.  모든 url에 대해서라면 <all_urls> 을 사용하면 됩니다.

 

action :  확장 프로그램을 클릭했을 떄 열리는 페이지에 대해서 정의합니다.

commands : 키보드 단축키를 등록하기 위해서 사용합니다. 사용할 수 키등이 존재하니  공식문서를 확인하길 바랍니다.

이때 각각의 키에 대해서 입력을 하면 추후 background(service_worker)에서  사용할 수 있습니다. 유의할 점은 _execute_action의 경우에는 팝업창을 여는데 사용됩니다. 

그리고 각각의 운영체제에 따른 입력 값을 지정할 수도 있습니다.

 

backgrond :  다른 페이지를 이용중일떄도 작동하게 할 내용(백그라운드에서)을 담을 수 있습니다.  저는 commands를 통해 등록한 키가 감지되면 background.js에서 특정 함수가 작동하도록 구현하였습니다.

content  : 컨텐츠 스크립트는 지정된 URL에서 실행됩니다., matches:를 통해서 특정 페이지에서만 스크립트가 실행하도록 하였습니다.

 

popup.html

팝업창의 경우 일반적인 html과 똑같습니다. 해당 페이지에서는 브라우저 api를 사용할 수 있습니다.(ex : alert) 

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <div>
      <label for="startInput">시작 시간 (예 : 11:00)</label>
      <input type="text" id="startInput" placeholder="11:00" maxlength="5" />
    </div>
    <div>
      <label for="endInput">종료 시간 (예: 13:00):</label>
      <input type="text" id="endInput" placeholder="15:00" maxlength="5" />
    </div>
    <button id="submit">시간 설정</button>
    <script src="popup.js"></script>
  </body>
</html>

 

 

이후  popup.js에서 버튼이 클릭될 경우 storage에 해당 시간을 저장하여 추후 다른 script에서도 사용할 수 있도록 하였습니다.

document.addEventListener('DOMContentLoaded', () => {
  const submitButton = document.getElementById('submit');
  const startInput = document.getElementById('startInput');
  const endInput = document.getElementById('endInput');

  submitButton.addEventListener('click', () => {
    const startTime = startInput.value.trim();
    const endTime = endInput.value.trim();

    if (!startTime || !endTime) {
      alert('시작 시간과 종료 시간을 모두 입력해주세요.');
      return;
    }

    if (!/^\d{2}:\d{2}$/.test(startTime) || !/^\d{2}:\d{2}$/.test(endTime)) {
      alert('시간 형식이 잘못되었습니다. 예: 11:00');
      return;
    }
	// chrome.storage api
    chrome.storage.local.set({ startTime, endTime }, () => {
      alert(`희망예약시간이 ${startInput} ~ ${endInput}으로 설정되었습니다`);
    });
  });
});

 

 

background.js

 

background.js에서 탭에 접근해서 돔 요소를 조종하려고 합니다. 하지만 일반적으로 tab에 접근할 경우에는 권한 문제가 발생합니다. activate_tabs를 통해서 간단하게 해결할 수도 있지만, 그럴 경우 사용자에게 불안감을 야기시킵니다.

따라서 크롬에서는 권한을 최소화 하는 방식을 권장합니다

이에 대한 대안이 content.js를 사용하는 방식입니다.

 

  • 콘텐츠 스크립트는 웹 페이지의 DOM과 상호작용할 수 있는 중간다리 역할을 합니다.
  • 백그라운드 스크립트에서 데이터를 전달받아 DOM을 수정하거나 이벤트를 실행합니다.

우선 코드를 먼저보겠습니다.

 

 

// 커맨드 감지 리스너
chrome.commands.onCommand.addListener((command) => {
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    if (tabs.length === 0) {
      notify('활성화된 탭이 없습니다.');
      return;
    }

    const tab = tabs[0];
    const url = tab.url || '';

    // 허용된 URL에서만 명령 실행
    if (!url.startsWith('url주소')) {
      notify('이 페이지에서는 확장 프로그램을 사용할 수 없습니다.');
      return;
    }

    // 명령 처리
    if (command === 'start_reservation') {
      chrome.storage.local.get(['startTime', 'endTime'], (data) => {
        const { startTime, endTime } = data;

        if (!startTime || !endTime) {
          notify('저장된 시간이 없습니다. 시간을 먼저 설정해주세요.');
          return;
        }

        const startNumeric = parseInt(startTime.replace(':', ''), 10);
        const endNumeric = parseInt(endTime.replace(':', ''), 10);

        // 콘텐츠 스크립트로 시간 범위를 전달
        chrome.scripting.executeScript(
          {
            target: { tabId: tab.id },
            files: ['content.js'],
          },
          () => {
            chrome.tabs.sendMessage(tab.id, {
              action: 'start_reservation',
              startNumeric,
              endNumeric,
            });
          }
        );
      });
    }

    if (command === 'fill_cert_no') {
      // 콘텐츠 스크립트에 fill_cert_no 액션 전달
      chrome.scripting.executeScript(
        {
          target: { tabId: tab.id },
          files: ['content.js'],
        },
        () => {
          chrome.tabs.sendMessage(tab.id, { action: 'fill_cert_no' });
        }
      );
    }
  });
});

function notify(message) {
  chrome.notifications.create({
    type: 'basic',
    iconUrl: '/images/icons-32.png',
    title: '알림',
    message: message,
  });
}

 

chrome.scripting.executeScript :  백그라운드 스크립트에서 특정 탭에 스크립트를 삽입할 때 사용됩니다. 이를 통해 웹 페이지의 DOM에 접근하거나, 콘텐츠 스크립트를 실행할 수 있습니다.

chrome.runtime.sendMessage :  크롬 확장 프로그램 내에서 서로 다른 스크립트 간에 메시지를 주고받는 데 사용

 

// content.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'start_reservation') {
    const { startNumeric, endNumeric } = message;

    // 예약 실행 로직
    executeReservation(startNumeric, endNumeric);
  }

  if (message.action === 'fill_cert_no') {
    // 인증 번호 입력 로직
    fillCertNo();
  }
});

 

이후 content.js에서도 리스너를 통해서 해당 함수를 사용합니다.

 

 

 

느낀점

환경자체가 다르고 사용하는 기능이 다르기 때문에 낯설게 느껴졌습니다. 모든 기능을 사용하거나 파악하기는 초기에 힘들기에 필요한 기능 위주로 검색하고 사용하는 것이 효율적이라고 느껴졌습니다.

 

모든 코드를 공개하지 않은 이유는.. 모든 프로젝트가 같은 로직을 사용하진 않을것이기 때문에, 그리고 저는 특정 사이트에만 적용되도록 구현했어서 공개할 필요는 없다고 생각했습니다. 다만 기본적인 원리 위주로  공개를 할 경우 타 프로젝트에 적용시에 더 유용하다 판단하였습니다.

 

728x90

들어가며...

 

최근 WebRTC 에 대한 관심이 생겨서 클론 코딩을 해보았다. 이를 정리하고자 포스팅을 시작했습니다.

WebRTC  : 출처 (MDN)
(Web Real-Time Communication)은 웹 애플리케이션과 사이트가 중간자 없이 브라우저 간에 오디오나 영상 미디어를 포착하고 마음대로 스트림할 뿐 아니라, 임의의 데이터도 교환할 수 있도록 하는 기술입니다

 

흔히 ZEP, gather 와 같은 메타버스 플랫폼 경우에 WebRTC를 활용하여 실시간 스트리밍을 가능하게 해줍니다.

 

 

1단계 장치 정보 받기

WebRTC로 P2P 통신을 하기 이전에 우선 장치 정보를 받아올 필요가 있습니다. 이때 사용하는 함수가
navigator.mediaDevices.getUserMedia 입니다. 이때 MediaDevices.enumerateDevices() 을 이용하면 그전 단계에서 

사용 가능한 디바이스 정보를 가져올 수 있습니다.

  const getMediaStream = useCallback(
    async (faceMode?: string) => {
      if (localStream) {
        return localStream;
      }

      try {
        const devices = await navigator.mediaDevices.enumerateDevices();
        const videoDevices = devices.filter(
          (divice) => divice.kind === 'videoinput'
        );
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
          video: {
            width: { min: 640, ideal: 1280, max: 1920 },
            height: { min: 360, ideal: 720, max: 1080 },
            frameRate: { min: 16, ideal: 30, max: 30 },
            facingMode: videoDevices.length > 0 ? faceMode : undefined,
          },
        });
        setLocalStream(stream);
        return stream;
      } catch (error) {
        console.log('failed to get stream', error);
        setLocalStream(null);
        return null;
      }
    },
    [localStream]
  );

 

 

2단계 PEER 연결하기

 

복잡한 P2P 연결에 대해서 알기 위해선 우선 네트워크에 관한 지식이 필요합니다. 비디오 스트리밍의 경우 데이터를 서버를 통해 직접적으로 전달하면 많은 부하가 생깁니다. 따라서 P2P 연결을 하는데, 이를 위해선 상대방의 IP 주소가 필요합니다. 이때 사용할 기술들은 보통 NAT, STUN 서버 ,ICE 입니다.

NAT (Network Address Translation)와 STUN

  • NAT는 사설 네트워크(private network) 내에서 사용하는 private IP 주소를 공인 IP(public IP 주소)로 변경하는 기술입니다. 이는 보안상 이유로 외부와의 직접적인 연결을 차단하고, 여러 장치가 하나의 공인 IP를 공유할 수 있게 해줍니다.
  • STUN (Session Traversal Utilities for NAT) 서버는 NAT 방화벽을 통과할 수 있도록 도와주는 역할을 합니다. 이 서버는 클라이언트에게 자신의 공인 IP 주소포트 번호를 알려주어, 외부에서 접근할 수 있도록 합니다. 하지만 STUN만으로는 NAT 뒤에 있는 장치 간의 연결을 완전히 해결할 수 없을 때도 있습니다.

ICE (Interactive Connectivity Establishment)

  • ICE는 WebRTC에서 두 피어 간의 연결을 설정할 때 최적의 경로를 찾는 프레임워크입니다. ICE는 STUN과 TURN 서버를 활용하여, NAT 뒤에 있는 두 장치가 서로 연결할 수 있도록 도와줍니다.
    • STUN은 NAT 방화벽을 통과할 수 있도록 공인 IP와 포트를 알려주는 데 사용됩니다.
    • TURN은 직접적인 연결이 불가능한 경우 중계 서버를 통해 연결을 지원합니다.

 

따라서 우리는 장치정보를 바탕으로 peer를 생성할 것 입니다. 이때 구글의 무료 스턴 서버를 사용하였고, 연결이 되면 peer의 stream을 통해서 p2p 연결 후 데이터를 주고 받습니다.

  const createPeer = useCallback(
    (stream: MediaStream, initiator: boolean) => {
      const iceServers: RTCIceServer[] = [
        {
          urls: [
            'stun:stun.1.google.com:19302',
            'stun:stun1.1.google.com:19302',
            'stun:stun2.1.google.com:19302',
            'stun:stun3.1.google.com:19302',
          ],
        },
      ];
      const peer = new Peer({
        stream,
        initiator,
        trickle: true,
        config: { iceServers },
      });

      peer.on('stream', (stream) => {
        setPeer((prevPeer) => {
          if (prevPeer) {
            return { ...prevPeer, stream };
          }
          return prevPeer;
        });
      });
      peer.on('error', console.error);
      peer.on('close', () => handleHangup({}));

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const rtcPeerConnection: RTCPeerConnection = (peer as any)._pc;

      rtcPeerConnection.onconnectionstatechange = async () => {
        if (
          rtcPeerConnection.iceConnectionState === 'disconnected' ||
          rtcPeerConnection.iceConnectionState === 'failed'
        ) {
          handleHangup({});
        }
      };

      return peer;
    },
    [handleHangup]
  );

 

 

때 보편적으로 RTCPeerConnection 를 통해서 연결을 합니다. 하지만 해당 프로젝트에서는  simple-peer 라이브러리를 활용하여 해당 단계를 쉽게 구현할 수 있습니다.

 

이를 통해서 스턴 서버를 통해서 ice 정보를 받고 스트리밍이 된다면 상대방의 stream 정보를 받아서 peer에 할당합니다. 

 

이후 해당 함수를 각각 전화거는 사람과 받는 사람이 사용하여 연결을 완료할 수 있습니다.

const completePeerConnection = useCallback(
    async (connectionData: {
      sdp: SignalData;
      ongoingCall: OngoingCall;
      isCaller: boolean;
    }) => {
      if (!localStream) {
        console.log('Missing the localStream');
        return;
      }

      if (peer) {
        peer.peerConnection?.signal(connectionData.sdp);
        return;
      }

      const newPeer = createPeer(localStream, true);

      setPeer({
        peerConnection: newPeer,
        partipantUser: connectionData.ongoingCall.participants.receiver,
        stream: undefined,
      });

      newPeer.on('signal', async (data: SignalData) => {
        if (socket) {
          // emit offer
          socket.emit('webrtcSignal', {
            sdp: data,
            ongoingCall,
            isCaller: true,
          });
        }
      });
    },
    [createPeer, localStream, ongoingCall, peer, socket]
  );

  const handleJoinCall = useCallback(
    async (ongoingCall: OngoingCall) => {
      setIsCallEnded(false);

      setOngoingCall((prev) => {
        if (prev) {
          return { ...prev, isRinging: false };
        }
        return prev;
      });
      const stream = await getMediaStream();
      if (!stream) {
        console.log('Could not get stream in handleJoinCall');
        return;
      }

      const newPeer = createPeer(stream, true);

      setPeer({
        peerConnection: newPeer,
        partipantUser: ongoingCall.participants.caller,
        stream: undefined,
      });

      newPeer.on('signal', async (data: SignalData) => {
        if (socket) {
          // emit offer
          socket.emit('webrtcSignal', {
            sdp: data,
            ongoingCall,
            isCaller: false,
          });
        }
      });
    },
    [createPeer, getMediaStream, socket]
  );

 

 

이때 ICE 정보의 경우 SDP(Session Description Protocol)에 담겨서 전송되고, 이는 Socket의 webritcSignal을 통해서 유저간 실시간으로 주고 받습니다. 그리고 해당 SDP 정보를 기반으로 p2p 연결을 하면 데이터가 stream 됩니다.

 

자세한 통화 프로젝트의 코드는 https://www.youtube.com/watch?v=dpWAqVSjgTM&list=PL63c_Ws9ecIS8ReV9MISpUGU71CK5cy0V 통해서 확인할 수 있습니다.

 

 

 

 

 

참고문헌 및 출처

https://webrtc.org/?hl=ko

 

WebRTC

An open framework for the web that enables Real-Time Communications (RTC) capabilities in the browser.

webrtc.org

 

https://developer.mozilla.org/ko/docs/Web/API/WebRTC_API

 

WebRTC API - Web API | MDN

WebRTC(Web Real-Time Communication)은 웹 애플리케이션과 사이트가 중간자 없이 브라우저 간에 오디오나 영상 미디어를 포착하고 마음대로 스트림할 뿐 아니라, 임의의 데이터도 교환할 수 있도록 하는

developer.mozilla.org

https://www.npmjs.com/package/simple-peer

 

simple-peer

Simple one-to-one WebRTC video/voice and data channels. Latest version: 9.11.1, last published: 3 years ago. Start using simple-peer in your project by running `npm i simple-peer`. There are 296 other projects in the npm registry using simple-peer.

www.npmjs.com

 

 

https://www.youtube.com/watch?v=dpWAqVSjgTM&list=PL63c_Ws9ecIS8ReV9MISpUGU71CK5cy0V

https://gh402.tistory.com/38

 

[WebRTC] WebRTC란 무엇일까?

🎞 WebRTC란 무엇인가? Web Real-Time Communication의 약자로 웹/앱에서 별다른 소프트웨어 없이 카메라, 마이크 등을 사용하여 실시간 커뮤니케이션을 제공해주는 기술이다. 우리가 잘 알고있는 화상통

gh402.tistory.com

https://gh402.tistory.com/45

 

[WebRTC] NAT, ICE, STUN, TURN 이란? ( WebRTC를 이해하기 위해 필요한 지식들)

WebRTC를 사용하기 전, 기본적으로 익혀야 할 지식들!! 🌎 NAT(Network Address Translation) '나'는 누구인지 '이름'으로 구별할 수 있듯, 각 기기에도 자신만의 이름이 있다. 그것이 바로 IP이고 이 IP는 고

gh402.tistory.com

 

 

+ Recent posts