한다면 프로젝트

개발 log·2021년 11월 23일
0

프로젝트

목록 보기
1/6
post-thumbnail

한번에 다 붙는 면접


📄 서비스 소개

코로나19로 인해 비대면 면접이 증가함에 따라 사용자 스스로 질문 리스트를 작성하여 자유롭게 연습할 수 있는 ‘비대면 모의 면접’ 서비스입니다.


🗓 진행 기간

2021.11.01 ~ 2021.11.04


🧨 기획 의도

코로나19로 인해 비대면 면접이 증가하며 현재 취준생들이 준비해야하는 면접환경이 오프라인보다 온라인방향으로 기우는 것을 본 저희 팀은 취준생들에게 자유롭게 모의면접을 보며 실제 면접에 대비할 수 있는 기능을 제공하고자 해당 프로젝트를 기획하게 되었습니다.


🎇 핵심 기능

1. 메인페이지

채용정보 및 최신 뉴스를 제공하고 모의 면접을 시작하거나 커스텀 면접 리스트를 만들 수 있습니다.

2. 면접 질문 작성하기

사용자가 직접 면접 질문 리스트를 작성할 수 있습니다.

3. 면접 설정

오디오와 비디오를 확인하며 면접관련 설정을 할 수 있습니다.

4. 면접 진행

카카오 API TTS를 통해 면접 질문을 음성으로 제공하며 사용자의 답변을 녹음하는 기능도 제공합니다.

5. 면접 결과

면접 시간을 기준으로 차트를 제공하며 녹음 파일을 다운받거나 들을 수 있습니다.



🎯 수행한 역할

1. TTS(text to speak)기능을 통해 면접자에게 질문내용 전달

면접 설정에서 전달받은 질문 리스트를 기반으로 면접질문을 음성으로 안내

2. 비디오 Web API 사용

실제 면접과 같이 면접자가 본인의 얼굴을 보고 진행하도록 navigatormediaDevices프로퍼티의 getUserMedia메서드를 통해 비디오 사용

3. 녹음 기능 제공

면접자 본인의 면접 내용을 사용자가 들어볼 수 있도록 녹음기능 제공



❗ 역할 진행 시 생긴 기술적인 문제점

1. 비동기 통신 시 생소한 타입으로 통신

TTS를 사용할 때 이전에는 주로 JSON타입의 파일을 주고받는 형식으로 비동기 통신을 했었는데 처음으로 XML타입의 파일을 보내고 arraybuffer형식의 타입을 전달받는 등 전혀 다른 타입의 파일을 처음 다루어보아 어려웠습니다.

2. 재사용성과 가독성의 합의점

같은 형태이지만 다른 기능을 하는 모달을 재사용하기 위해 이벤트 핸들러에 if문을 많이 사용하며 코드가 복잡해지는 경향이 있었는데 이를 깔끔하게 정리하기가 어려웠습니다.

3. 녹음 파일 전송

녹음 기능을 제공함에 있어 MediaRecorder를 사용하여 녹음과 녹음 파일 생성까지는 어렵지 않았으나 이를 결과페이지로 전송해야하는 과정에서 어려움이 있었습니다.



👍 문제 해결 방안

1. 비동기 통신 시 생소한 타입으로 통신

const xmlData = `<speak>${questionList[currentInterviewNum - 1]}</speak>`;
      const { data } = await axios.post('https://kakaoi-newtone-openapi.kakao.com/v1/synthesize', xmlData, {
        headers: {
          'Content-Type': 'application/xml',
          Authorization: progress.env.AUTH,
        },
        responseType: 'arraybuffer',
      });
      const context = new AudioContext();

      context.resume();
      context.decodeAudioData(data, buffer => {
        const source = context.createBufferSource();
        source.buffer = buffer;
        source.connect(context.destination);
        source.start(0);
      });

  1. 전달받은 질문리스트의 현재 진행중인 문제번호를 적용하여 xmlData 생성
const xmlData = `<speak>${questionList[currentInterviewNum - 1]}</speak>`;
  1. kakao API TTS를 제공하는 URL에 xmlData를 전송하고 arraybuffer를 응답 받음
const { data } = await axios.post('https://kakaoi-newtone-openapi.kakao.com/v1/synthesize', xmlData, {
        headers: {
          'Content-Type': 'application/xml',
          Authorization: progress.env.AUTH,
        },
        responseType: 'arraybuffer',
      });
  1. 음성을 재생할 audiocontext 생성
const context = new AudioContext();
  1. 음성 권한을 수락하고 디코딩을 통해 음성 재생
context.resume();
context.decodeAudioData(data, buffer => {
  const source = context.createBufferSource();
  source.buffer = buffer;
  source.connect(context.destination);
  source.start(0);
});

2. 재사용성과 가독성의 합의점


$modalActionButton.onclick = async e => {
  const { type } = e.currentTarget.dataset;

  if (mediaRecorder.state !== 'inactive') mediaRecorder.stop();
  mediaRecorder.start();

  addRepeatOfMediaRecorder(type)
  addInterviewNum(type)

  try {
    makeInterviewResult(type)
    playInterview(type)
    timer.stop();
    timer.setTime(interviewTime);
    timer.start(setInterview, 1000);
    $interviewAudioIconState.classList.add('audio-run');
    $modalOuter.classList.toggle('hidden', true);
  } catch (e) {
    console.error(e.message);
  }
  
};

type을 전달받아 상황별로 분류하는 함수를 호출하여 역할을 분리

3. 녹음 파일 전송

  mediaRecorder.ondataavailable = ({data}) => {
    chunks.push(data);
  };

음성데이터가 준비될때마다 발생하는 ondataavailable이벤트를 잡아서 chunk배열에 push

  mediaRecorder.onstop = async () => {
    const blob = new Blob(chunks, {
      type: 'audio/ogg codecs=opus',
    });
    chunks = [];
    await blob.arrayBuffer().then(res => {
      const byteArray = new Uint8Array(res);

      recordList.push([...byteArray].join(','));
      if (mediaRecorder.repeat) {
        recordList.pop();
        mediaRecorder.repeat = null;
      }
    });
  };

mediaRecorder의 onstop 이벤트를 잡아서 chunk배열에 담긴 데이터로 audio/ogg타입의 blob객체를 생성하고 이를 다시 arraybuffer => Uint8Array를 만들어서 결과 페이지로 전송할 interviewResult객체에 담아두고 mediaRecorder에 repeat속성이 있다면 recordList에서 pop하고 repeat속성을 초기화한다.

await axios.put(router.interview, interviewResult, { maxBodyLength: Infinity });

이렇게 생성된 interviewResult를 결과페이지로 전송



✔ 아쉬운 점

1. 비디오, 오디오 예외 처리

초기 목적은 비디오가 가능한 사용자만 받자는 전제하에 면접설정과 면접진행 초기 모달에서 비디오권한을 확인하여 버튼을 활성화 시키는 로직이 있는데 비디오나 오디오가 고장난 경우에도 사용자는 진행을 하고 싶을 수 있다는 것을 간과하여 예외처리를 제대로 못해준 것이 아쉽습니다.

2. 재사용성은 높지만 가독성이 낮은 모달

같은 템플릿을 공유하지만 기능이 다른 모달들의 이벤트를 핸들링할 때 함수로 구분하여 분리하고 클래스나 생성자 함수로 만들어 관리했다면 좀 더 편했을 것 같았지만 제대로 분리해내지 못한 것이 아쉽습니다.

3. 녹음 파일 전송

녹음 기능을 제공한 후 녹음 파일을 다른 페이지로 전송할 때 파일은 파일로써 다루어야 하는데 저희 방식대로 해석해서 통신을 무겁게 한 점이 아쉽습니다.

🍳 아쉬운 점 개선방향

1. 비디오, 오디오 예외 처리

비디오와 오디오의 권환설정이 제대로 이뤄지지 않으면 진행을 못하는 것이 아닌 비디오의 경우 대체이미지를 제공하고 오디오의 경우 녹음기능을 제공하지 않는 방식으로 개선하고자 함

2. 재사용성은 높지만 가독성이 낮은 모달

클래스나 생성자 함수로 묶어 추상화한 후 인스턴스를 생성하여 사용하는 방식으로 수정 및 기능 분리는 메서드로 구현하는 방향으로 개선하고자 함

3. 녹음 파일 전송

파일 전송 관련 라이브러리를 활용하여 파일은 파일로써 다루는 방식으로 개선하고자 함



👍 프로젝트를 통해 알게 된 점

1. 비동기 통신

저는 원래 비동기와 동기를 비교하며 비동기 방식은 순서만 보장되지 않을뿐이지 속도측면에서 우수하니 당연히 비동기 방식이 좋은게 아닌가? 라는 생각을 갖고 있었습니다.
하지만 이번 프로젝트에서 비동기적으로 받아와야하는 데이터나 API를 다뤄보며 순서가 보장되는 것이 얼마나 중요한지 알게되었습니다.

2. 음성 데이터 관리

이번 프로젝트에서 음성관련으로 API나 파일을 다뤄야 할 일이 많았는데 MediaRecorder로 음성 데이터를 저장할 때는 음성데이터를 생성한 URL 내에서만 유효한 것을 알게 되었습니다.
이를 전송하여 다른 URL에서 사용할 수 있도록 하기 위해 저희는 원본 데이터 자체를 보내는 형식으로 구현했는데 추후 검색을 통해 파일은 파일로서 다뤄야한다는 것을 알게되어 나중에는 multer같은 라이브러리를 사용하여 파일을 전송하는 것도 해보고 싶습니다.

profile
개발자 지망생

0개의 댓글