
RadioPremium은 라디오 음성 콘텐츠에서 광고를 탐지해 자동으로 채널을 전환하고,
광고가 끝나면 다시 원래 채널로 복귀하는 서비스입니다.
청취자가 반복적인 광고 없이 콘텐츠에 온전히 몰입할 수 있도록, 끊김 없는 라디오 환경을 제공합니다.
라디오는 여전히 많은 사람들이 일상 속에서 자연스럽게 접하는 매체입니다.
가정에서 다 함께 듣거나, 직장에서 배경음처럼 틀어놓는 경우도 많습니다.
이처럼 여러 사람이 함께 듣는 상황이라면, 원하지 않는 광고를 그대로 들어야만 하는 불편함이 생깁니다.
광고에 강제로 노출되는 일이 반복되다 보면, 자연스럽게 방송 내용에도 집중이 흐트러지고
"광고가 많은 매체"라는 인식과 함께 라디오는 점점 흥미를 잃게 됩니다.
A 채널을 듣다가 광고가 나오면, 광고가 없는 B 채널로 자동으로 넘어갈 수는 없을까?
광고 구간만 피해가면서, 콘텐츠만 끊김 없이 들을 수는 없을까?
만약 그럴 수 있다면, 라디오는 훨씬 더 편리하고 즐거운 매체가 될 수 있을 거라 생각했습니다.
그래서 저희는 누구나 한 번쯤 느껴봤을 작지만 반복적인 불편함을 기술로 해결해보고자, 이 프로젝트를 기획하게 되었습니다.

본 시스템은 총 5개의 주요 컴포넌트로 구성되어 있습니다.
- 사용자 (청취자)
- 클라이언트
- Express 서버
- Whisper 서버
- Supabase DB
아래는 컴포넌트 간의 데이터 흐름과 그 내부에서 수행되는 역할을 단계별로 설명한 내용입니다.
사용자가 라디오 방송 채널을 선택합니다.
클라이언트는 사용자가 선택한 채널 정보를 Express 서버에 요청합니다.
Express 서버는 Whisper 서버에 채널의 m3u8 스트림 주소를 전달합니다.
Whisper 서버는 내부적으로 ffmpeg
를 사용하여 스트리밍 오디오를 실시간으로 수신하고,
Whisper 모델을 통해 음성 데이터를 텍스트로 변환합니다.
Whisper 서버는 변환된 텍스트와 함께,
내부에 학습된 XGBoost 모델을 사용하여 해당 문장이 광고일 가능성(isAd
)과
그 판단에 대한 신뢰도(confidence
)를 Express 서버에 전송합니다.
Whisper 분석 결과가 isAd: false
이지만, confidence
값이 신뢰도가 낮은 경우에는
Express 서버가 추가적인 판단을 위해 Supabase 데이터베이스에 저장된 광고 키워드 목록을 조회합니다.
이 목록은 사전에 수집돤 광고 문구 기반의 키워드들로 구성되어 있습니다.
Supabase는 광고 키워드 목록을 응답하며,
Express 서버는 Whisper가 전달한 텍스트 내에 해당 키워드들이 포함되어 있는지를 검사합니다.
만약 하나 이상의 키워드가 포함되어 있다면, 해당 문장을 광고로 보정 판단하여
isAd
값을 true
로 설정합니다.
광고로 최종 판단되었을 경우, Express 서버는 클라이언트로 { isAd: true }
메시지를 전달합니다.
클라이언트는 해당 메시지를 수신하고, 즉시 광고가 재생되지 않는 채널로 전환합니다.
![]() |
라디오 채널 목록
라디오 프리미엄에서 제공하는 다양한 라디오 채널을 확인할 수 있습니다.
채널 이름과 로고를 확인하고, 원하는 채널을 클릭하면 바로 청취할 수 있습니다. 즐겨찾기 기능
즐겨찾기 기능을 통해 자주 듣는 채널을 쉽게 관리할 수 있습니다. 기능 요약
|
![]() |
실시간 키워드 기반 검색 기능
원하는 채널을 빠르게 찾을 수 있도록, 실시간 검색 기능을 제공합니다.
기능 요약
|
![]() |
사용자 맞춤 환경설정 기능
라디오 사용 경험을 개인 취향에 맞게 조절할 수 있도록
현재 제공되는 설정은 다음과 같으며,
기능 요약
|
서비스 전반에 걸쳐 전역으로 관리해야 하는 상태가 다수 존재했고,
프로젝트의 규모와 빠듯한 개발 일정 속에서 빠르게 구현하고 유지보수할 수 있는 상태 관리 도구가 필요했습니다.
Zustand는 Redux에 비해 구조가 단순하고 보일러플레이트 코드가 적어,
짧은 시간 안에 직관적으로 구현할 수 있어 개발 효율성이 높았습니다.
또한 useStore()
만으로 필요한 상태에 간단히 접근할 수 있고,
컴포넌트 단위로 리렌더링을 최소화할 수 있어 실시간 기능이 많은 프로젝트에 성능상 유리하다고 판단했습니다.
무엇보다 사용자 수가 많고 꾸준히 유지보수되고 있는 안정적인 오픈소스 라이브러리라는 점에서 신뢰할 수 있었고,
Zustand는 이러한 조건을 모두 만족시키는 적합한 상태 관리 도구였습니다.
사용자 정보, 청취 이력, 광고 탐지 결과 등 정형화된 데이터를 안정적으로 관리할 수 있는 관계형 데이터베이스가 필요했습니다.
PostgreSQL은 데이터 간 관계 정의 및 제약 조건 설정이 유연해
복잡한 데이터 구조도 일관성 있게 유지할 수 있었고, 안정적인 데이터 흐름을 구성하는 데 효과적이었습니다.
관계형 데이터베이스 중에서도 대표적인 오픈소스인 MariaDB와 비교했을 때,
PostgreSQL은 Supabase와의 통합 환경이 잘 구축되어 있어 실시간 감지 및 구독 기능을 간편하게 구현할 수 있다는 점에서 개발 효율성이 더 높았습니다.
이처럼 정형 데이터 처리에 적합하면서 Supabase 실시간 기능과의 연계가 자연스러운 PostgreSQL가 프로젝트에 적합한 데이터베이스라고 판단했습니다.
라디오는 음성 기반 콘텐츠이기 때문에 광고 여부를 판별하려면 실시간으로 라디오 내용을 텍스트로 변환 후 분석하는 과정을 거쳐야 합니다.
이를 위해 음성을 텍스트로 변환해주는 Whisper 모델(이하 Whisper)을 활용하여,
사용자가 듣고 있는 라디오에서 나오는 음성을 텍스트로 변환하고, 해당 문장에 광고 관련 키워드가 포함되어 있는지를 분석합니다.
이 과정에서 먼저, 사용자가 청취 중인 라디오 스트리밍 주소(URL)를 받아 ffmpeg
를 통해 라디오 음성 데이터를 실시간으로 수신합니다.
Note
ffmpeg는 오디오·비디오를 처리하는 툴로 라디오 스트림 URL을 실시간으로 오디오로 변환해줍니다.
Whisper는 ffmpeg
를 통해 수신한 음성 데이터에서 텍스트를 추출합니다.
Whisper로 음성 데이터를 텍스트로 변환한 후, 해당 텍스트를 XGBoost
를 통해 학습시킨 모델을 사용하여 광고 여부와 신뢰도를 예측한 결과를 백엔드로 전달합니다.
Note
XGBoost는 머신러닝 알고리즘 중 하나입니다. 많은 데이터에서 패턴을 학습한 뒤, 새로운 문장이 들어오면 해당 텍스트가 광고일 가능성이 얼마나 되는지를 예측해줍니다.
학습시킨 모델을 사용하여 전달받은 결과에는 광고 여부와 신뢰도 값이 포함되는데
이 신뢰도가 낮은 경우는 광고임을 확신할 수 없고, 실제 광고인데도 감지되지 않는 문제가 발생할 수 있습니다.
이를 보완하기 위해, 다음과 같은 보정 로직을 추가했습니다.
if (!isAd && confidence < 0.75) {
const keywordList = getAdKeywords();
const matched = keywordList.find(({ keyword }) => text.includes(keyword));
if (matched) {
isAd = true;
}
}
- 학습시킨 모델이 광고가 아니라고 판단했고 (
!isAd
) - 동시에 신뢰도도 낮은 경우 (
confidence < 0.75
) - 광고 키워드 목록에 포함된 단어가 있는지 검사
- 만약 광고 키워드가 포함되어 있다면 광고라고 판단(
isAd = true
)
이 로직을 통해 일반 멘트라고 판단한 문장도 광고라고 보정할 수 있기 때문에 광고를 놓치는 확률을 줄이고 광고 판단의 정확도를 높일 수 있습니다.
광고라고 판단되면 아래와 같이 광고를 감지한 이력을 기록하여 같은 채널의 광고에 대해 중복 알림을 보내지 않도록 합니다.
const userChannelKey = `${userId}:${channelId}`;
- 이 key는 어떤 사용자가(userId), 어떤 채널에서(channelId) 광고를 들었는지를 식별하는 기준이 됩니다.
해당 채널에서 광고가 감지되었다면 아래 로직이 실행됩니다.
if (!isAdPlaying.get(userChannelKey)) {
isAdPlaying.set(userChannelKey, true);
io.to(socketId).emit("radioText", { isAd: true }); // 프론트에 광고 감지 신호 전송
}
- 사용자가 듣고 있는 채널에서 광고가 감지되었지만 광고 감지 이력이 없을 경우(
!isAdPlaying.get(userChannelKey)
) - 해당 광고 감지 이력을 기록 (
isAdPlaying.set(userChannelKey, true)
) - 사용자에게 광고 재생 중 신호 전송(
emit("radioText", { isAd: true })
)
위 로직을 통해 백엔드는 광고가 최초로 감지된 시점에만 프론트로 채널 전환 신호를 보내며,
이미 광고가 감지된 채널이라면 추가로 감지된 광고는 무시하기 때문에 불필요한 채널 전환을 방지할 수 있습니다.
백엔드에서 socket.emit("radioText", { isAd: true })
메시지를 전송하면,
프론트는 해당 메시지를 받아 광고가 감지되었음을 인지하고 즉시 채널 전환을 수행합니다.
전환 과정은 다음 두 단계로 이루어집니다.
프론트는 현재 듣고 있던 채널 정보를 저장한 후 새로운 채널로 전환할 준비를 합니다.
- 사용자가 미리 우회 채널을 지정한 경우 → 해당 채널로 이동
- 우회 채널이 설정되어 있지 않은 경우 → 백엔드에 무작위로 선택된 "광고 없는 채널" 정보를 요청하여 이동
이때 현재 채널 정보는 이전 채널 정보(prevChannelId
)에 저장하여 나중에 광고가 끝났을 때 복귀할 수 있도록 사용됩니다.
setPrevChannelId(selectedChannelId);
setIsChannelChanged(true);
새로운 채널이 결정되면 실제 재생 중인 <video>
요소의 스트리밍 주소를 교체합니다.
Important
<audio>
태그가 아닌 <video>
태그를 사용한 이유
이 프로젝트에서는 라디오를 재생하기 위해 <video>
태그를 사용했습니다.
일반적으로 오디오 콘텐츠에는 <audio>
태그가 더 적합하지만, 다음과 같은 이유로 <video>
를 선택했습니다:
📡 HLS 스트리밍 호환성
라디오는 HLS (HTTP Live Streaming) 형식으로 제공되며, 브라우저의 <audio>
태그는 HLS를 직접 지원하지 않는 경우가 많습니다.
반면 <video>
태그는 대부분의 최신 브라우저에서 HLS 스트림을 더 안정적으로 재생할 수 있습니다.
기존 오디오 스트림을 종료하고 선택된 채널의 URL을 <video>
에 적용하여, 사용자 입장에서는 끊김 없이 새로운 채널로 곧바로 재생됩니다.
광고 감지로 채널이 이동된 후,
백엔드에서 기존 채널의 광고 여부를 모니터링하는 과정에 광고가 종료되었다고 판단되면 다음과 같은 로직을 수행합니다.
isAdPlaying.set(userChannelKey, false);
io.to(socketId).emit("radioText", { isAd: false });
이처럼 광고가 종료된 시점에만 프론트로 기존 채널 복귀 신호를 보냅니다.
백엔드에서 기존 채널의 광고가 종료되었다고 판단하여 socket.emit("radioText", { isAd: false })
메시지를 전송하면,
프론트는 해당 메시지를 받아 기존 채널의 광고가 종료되었음을 인지하고 기존 채널로 복귀합니다.
이때 기존 채널로 복귀하면 이전 채널 정보(prevChannelId
)를 초기화하고 채널 이동 여부(setIsChannelChanged
)를 false
로 변경하여
이후 광고가 다시 재생될 때, 다른 채널로 이동 로직이 정상적으로 동작할 수 있도록 설정합니다.
setPrevChannelId(null);
setIsChannelChanged(false);
처음에는 라디오 음성에서 텍스트를 추출한 후, 그 안에 광고 관련 키워드가 포함되어 있는지를 기준으로 광고 여부를 판단했습니다.
예를 들어, “주택청약”, “즉시할인”, "한국농어촌공사" 같은 단어가 들어가 있으면 광고일 가능성이 높다고 판단하는 방식이었습니다.
하지만 음성을 텍스트로 변환해주는 Whisper는 같은 말도 매번 다르게 인식하는 경우가 많았고,
광고 문구도 다양한 표현으로 바뀌기 때문에 단순한 키워드 탐지 방식만으로는 광고를 정확히 구분하기 어려웠습니다.
이 한계를 극복하기 위해, 저희는 머신러닝을 도입해 텍스트 자체를 분석하고 자동으로 광고 여부를 분류하는 방법을 시도하게 되었습니다.
처음에는 로지스틱 회귀라는 간단한 분류 모델을 사용했습니다.
이 모델은 특정 단어나 특징이 있으면 광고일 확률이 높다고 계산하는 방식으로 작동합니다.
빠르고 직관적이지만, 문장의 흐름이나 다양한 표현 방식을 잘 반영하지는 못합니다.
실제 실험에서도 광고 탐지 정확도(특히 광고를 놓치지 않는 비율, 즉 재현율)가 50~65% 수준에 머물렀고,
광고임에도 광고라고 판단되지 않는 한계가 존재했습니다.
더 정확한 탐지를 위해 저희는 XGBoost라는 비선형 트리 기반 머신러닝 모델로 전환했습니다.
이 모델은 단어 하나 하나뿐만 아니라, 여러 단어가 어떤 식으로 조합되고, 문장에서 어떤 흐름으로 사용되는지까지 고려할 수 있습니다.
이에 따라 Whisper가 같은 말이어도 다른 텍스트로 추출되는 문제에 대응할 수 있습니다.
예를 들어 “주택청악”, "즉씨할인"처럼 텍스트를 정확하게 추출하지 못하더라도,
선형 모델보다 문맥, 표현 및 패턴이 다양하고 복잡할 가능성에 적합한 비선형 트리 모델을 사용하여 정확도를 높일 수 있었습니다.
이러한 개선 과정을 거치면서, 키워드 방식 단독으로는 약 75% 수준의 광고 탐지 정확도,
머신러닝 모델 단독으로는 약 79% 수준의 탐지 정확도를 보였습니다.
하지만 광고 텍스트의 특성상, 패턴이 일정하지 않거나 빈도가 낮은 표현,
혹은 신조어나 고유명사처럼 학습 데이터에 부족한 단어들은 머신러닝 모델 하나만으로 잡아내기 어려운 경우가 있었습니다.
반면, 키워드 방식은 모델이 학습하지 못한 특이 케이스나 명확한 단어 중심의 광고를 빠르게 포착할 수 있다는 장점이 있습니다.
이러한 상호 보완적인 특성을 활용하여,
머신러닝 모델과 키워드 탐지 방식을 함께 적용하는 하이브리드 방식을 도입하였고,
그 결과 광고 탐지 정확도가 약 86% 수준까지 향상되었습니다.
사용자가 라디오를 듣는 도중 광고가 감지되면, 서버에서 이를 알려주고 자동으로 채널을 전환합니다.
이를 위해 프론트와 서버는 실시간으로 정보를 주고받습니다.
하지만 간헐적으로, 광고가 감지되었음에도 사용자에게 광고 감지 신호가 전달되지 않는 문제가 발생했습니다.
서버 연결이 정상적으로 되어 있는 것처럼 보였지만, 광고 감지 기능이 작동하지 않았습니다.
서버가 광고 감지 시 사용자에게 알림을 보내기 위해서는, 먼저 해당 사용자가 누구인지(userId
)를 알고 있어야 합니다.
이를 위해 프론트에서는 서버에 접속할 때 다음과 같은 신호를 보냅니다.
socket.emit("registerUser", { userId });
서버는 이 신호를 수신해 해당 사용자를 기억합니다.
그런데 문제는, 이 registerUser 신호가 일부 상황에서는 서버에 도달하지 않는다는 점이었습니다.
예를 들어,
- 사용자가 이미 서버와 연결된 상태에서 페이지를 이동하거나 새로고침할 경우,
- 소켓 연결은 유지되지만, 서버는 “해당 사용자가 누구인지” 모르는 상태가 됩니다.
- 이로 인해 광고가 감지되어도 누구에게 알릴지를 몰라서 아무 동작도 일어나지 않는 문제가 발생했습니다.
- 서버 로그를 통해, 문제가 발생했을 때
userId
가 존재하지 않는 것을 확인했습니다. - 이는 클라이언트에서 registerUser 신호를 보내지 않아 발생한 문제였습니다.
기존에는 아래 코드처럼 "connect" 이벤트가 발생했을 때만 registerUser를 전송하고 있었습니다.
socket.on("connect", () => {
socket.emit("registerUser", { userId });
});
이 방식은 다음과 같은 문제가 있었습니다.
- 사용자가 이미 소켓에 연결된 상태에서 페이지가 재실행되면 "connect" 이벤트가 발생하지 않습니다.
- 이로 인해 서버는 해당 사용자를 인식하지 못하고, 신호를 전달하지 못합니다.
기존 구조는 유지하되, 다음을 추가로 보완했습니다.
- 소켓이 이미 연결된 상태(
socket.connected === true
)인 경우, registerUser를 즉시 한 번 더 전송하여 서버가 사용자를 인식하도록 처리했습니다.
useEffect(() => {
const handleConnect = () => {
socket.emit("registerUser", { userId });
console.log("✅ registerUser 전송:", userId);
};
socket.on("connect", handleConnect);
// 이미 연결되어 있다면 즉시 registerUser 호출
if (socket.connected) {
handleConnect();
}
return () => {
socket.off("connect", handleConnect);
};
}, [userId]);
- 이 문제는 서버는 연결되어 있는데 사용자가 누군지 모르는 상태에서 발생한 문제였습니다.
- 이를 해결하기 위해, 이미 연결된 경우(페이지 새로고침)에도 한 번 더 사용자 정보를 전송하는 전략을 도입했습니다.
- 그 결과, 서버는 사용자와 정확히 연결 상태를 유지할 수 있게 되었고, 광고 감지 결과를 빠짐없이 전달할 수 있게 되었습니다.
RadioPremium은 사용자 1이 A 채널을 듣고 있을 때 광고가 감지될 경우 시스템이 자동으로 광고가 없는 다른 채널 (B)로 이동시킵니다.
하지만 이 과정에서 다음과 같은 문제가 발생했습니다.
만약, 사용자 2가 C 채널 청취 중 광고가 나와도 이를 감지하지 못합니다.
사용자 1의 A 채널 광고 감지가 사용자 2의 채널 탐지 흐름까지 방해하는 구조였기 때문입니다.
이 문제는 광고 재생 여부를 사용자나 채널 별로 관리하지 않고, 하나의 상태 값으로 처리하면서 발생한 로직 오류입니다.
기존 코드 구조
let isAdPlaying = false;
isAdPlaying
은 모든 사용자와 채널을 통틀어 하나의 상태로 존재하는 값이었습니다.
이 구조에서는 어떤 사용자가 어느 채널에서 광고를 듣고 있는지를 알 수 없습니다.
즉, 광고가 감지되면 isAdPlaying = true
로 하나의 상태로 설정되고, 이후 다른 사용자 또는 채널에서 광고가 감지되어도
이미 광고가 감지된 것으로 판단하여 탐지가 무시되거나 전환이 수행되지 않는 문제가 발생합니다.
문제가 발생하는 흐름 예시
① 사용자 1의 A 채널 → 광고 감지 → B 채널로 자동 이동
② 사용자 2가 C 채널을 청취 중
③
isAdPlaying
은 채널과 사용자를 구분하지 않는 상태 값이기 때문에④ 사용자 2의 C 채널에서 광고가 재생되어도 시스템이 감지하지 못함 → 채널 자동 전환 실패
이 문제를 해결하기 위해 광고 감지 상태를 아래와 같이 사용자 + 채널 단위로 분리했습니다.
const isAdPlaying = new Map();
isAdPlaying.set("user1:A", true); // key: `${userId}:${channelId}`, value: boolean
이 구조를 사용하면 특정 사용자에게 특정 채널에서 광고가 감지되었는지를 개별적으로 기록할 수 있습니다.
예를 들어 사용자 user1이 A 채널에서 광고를 듣고 있는 경우 user1:A
키가 true
로 기록됩니다.
이를 통해 사용자 1의 A 채널에서 광고가 감지되더라도, 사용자 2가 B 채널을 듣고 있을 때는 전혀 영향을 받지 않고,
정상적으로 B 채널에서 광고가 재생될 경우 해당 사용자 기준으로만 감지 및 전환 로직이 작동합니다.
아래는 사용자 + 채널 단위 구조를 바탕으로 사용자의 현재 채널 상태에 맞춰 광고 여부를 판단하는 코드입니다.
const userChannelKey = `${userId}:${channelId}`;
if (!isAdPlaying.get(userChannelKey)) {
isAdPlaying.set(userChannelKey, true);
}
광고 감지 여부는 사용자-채널 조합마다 개별적으로 관리되며, 동시에 여러 사용자가 서로 다른 채널을 듣고 있어도 각자에게 맞는 광고 감지 및 전환이 안정적으로 작동하게 됩니다.
이 구조 변경을 통해 사용자가 많아질수록 생길 수 있는 감지 충돌 문제를 해결하고, 보다 정확하고 사용자 맞춤형 채널 전환이 가능해졌습니다.
이재윤
이번 팀 프로젝트는 단순한 구현을 넘어, 기획부터 배포까지 개발 사이클 전반을 직접 경험해볼 수 있었던 값진 시간이었습니다. 처음 아이디어를 도출하는 과정부터 쉽지 않았고, AI의 도움을 받아보기도 했지만 결국 가장 중요한 건 사용자의 문제를 이해하고 해결하는 방향을 스스로 정의하는 것이었습니다. 이 과정에서 AI로는 대체하기 어려운 창의적인 사고와 문제 정의 능력의 중요성을 다시금 깨달았습니다.이후 POC와 칸반 작성을 거치며 아이디어를 실제로 구현 가능한 형태로 구체화했고, 기능 단위를 세분화하고 정리하면서 기획과 설계의 실무적 감각과 협업을 위한 명확한 커뮤니케이션의 필요성을 깊이 있게 체험했습니다.
특히 협업의 중요성을 절실하게 느낄 수 있었습니다. 어떤 아이디어든 제안하거나 반대할 때에는 타당한 논리와 근거를 통해 팀을 설득해야만 실제로 반영할 수 있었고, 그 과정에서 의견 충돌을 조율하고 합의점을 찾아가는 경험은 협업 역량을 키우는 데 큰 도움이 되었습니다. 기능 개발에만 집중할 수 없다는 점도 인상 깊었습니다. Pull Request 리뷰, 갑작스러운 이슈 대응, 문서 정리, 빠른 회의와 결정 등 다양한 업무**를 함께 병행하면서 협업 능력에 개발 실력 뿐만 아니라 다른 역량도 중요하다는 것을 배우게 되었습니다.
이 과정에서 ‘나는 좋은 팀원인가?’라는 질문을 자주 던지며, 단지 결과를 내는 것을 넘어 팀 안에서 신뢰받는 구성원이 되기 위해 어떤 태도를 가져야 하는지 고민하고 실천하려 노력했습니다. 먼저 사과하기, 회의 분위기 환기하기 같은 작은 행동들도 팀워크를 위해 스스로 할 수 있는 노력이라고 생각했습니다.
비록 쉽지 않은 과정이었지만, 팀원들과 함께 고민하고 만들어가며 기술 역량뿐 아니라 소통, 협업, 문제 해결 능력까지 성장할 수 있었던 소중한 경험이었습니다. 이 프로젝트에서 느낀 점과 배운 것들을 바탕으로, 앞으로 더 좋은 개발자가 되기 위한 발판을 다질 수 있었습니다.
이다은
처음엔 그저 "광고를 피하는 라디오 서비스"라는 아이디어에서 시작했지만, 그것을 함께 구현한다는 것은 생각보다 훨씬 더 깊은 일이었습니다.이번 프로젝트에서 협업의 규칙과 구조부터 함께 정의했고, 작은 선택 하나조차 팀원들과 함께 논의하며 결정했습니다. 그 과정 속에서 처음 구상했던 방향보다 더 나은 해결책이 자연스럽게 만들어졌고, 아이디어에 불과했던 서비스는 각자의 시선과 손을 거치며 점점 구체적인 형태를 갖춰갔습니다.
물론 모든 선택이 매끄럽게 정리되진 않았습니다. 작은 UI 요소나 로직의 흐름을 정하는 과정에서도 서로 다른 관점이 충돌하는 순간들이 있었고, 그럴 때마다 다양한 의견을 조율하며 더 나은 방향을 함께 찾아야 했습니다. 이 과정에서 타인의 관점을 존중하면서도, 단순한 주장에 그치지 않고 신뢰할 수 있는 자료와 근거를 갖추어 의견을 설명하는 태도를 배웠습니다. 내가 옳음을 증명하기보다 우리 모두에게 맞는 방향을 함께 찾아가는 방법을 익힐 수 있는 기회였습니다. 앞으로 누군가와의 의견 차이를 마주하더라도 본질에 집중하며 합리적인 방향을 함께 찾아갈 수 있는 힘이 될 것이라 믿습니다.
RadioPremium은 광고를 탐지하기 위해 Whisper 모델·서버·클라이언트를 하나의 흐름으로 연결해야 했기에 각 시스템이 언제 어떤 데이터를 주고받을지 직접 설계하고 조율해야 했습니다. 그 과정에서 단순한 CRUD를 넘어 데이터 흐름 전체를 바라보는 감각을 키울 수 있었고, 실시간 이벤트 처리 방식에 대한 이해도 함께 깊어졌습니다.
다만 아쉬운 점도 있었습니다. 광고 감지 후 채널을 전환하고 복귀하는 흐름은 구현했지만, 그 변화가 사용자에게 어떻게 보여질지는 충분히 고려하지 못했습니다. 기능 구현만큼이나 사용자가 왜 이런 동작이 일어났는지를 자연스럽게 이해할 수 있도록 설계해야겠다고 느꼈습니다.
이번 프로젝트를 진행하며 단순한 개발을 넘어, 협업의 본질에 대해 깊이 고민할 수 있었습니다. 완벽하진 않았더라도 팀원들과 끝까지 조율하며 만들어낸 이 결과물은 분명 값진 경험으로 남았습니다. 앞으로도 각자의 다름을 존중하며, 함께 더 나은 방향을 만들어가는 협업을 이어가고 싶습니다.
오희주
팀 프로젝트는 처음부터 정해진 구조나 룰이 없었기 때문에, 구현 하나하나를 모두 팀이 함께 결정해야 했습니다.작은 디렉터리 구조부터 큰 설계 방향까지 모든 것을 조율해야 했고, 각자의 기준과 익숙한 방식이 달라 초반에는 의견을 맞춰가는 과정이 쉽지 않았습니다.
초기에는 개인적인 경험에 기반한 의견들이 많았지만, 점차 더 객관적인 기준을 만들기 위해 대기업에서 사용하는 방식이나 GitHub에서 추천을 많이 받은 오픈소스 레포지토리 사례를 참고하게 되었습니다. 그 이후에는 감정이 아닌 근거 기반의 논의가 가능해졌고, 의견 충돌이 생기더라도 서로의 논리적인 의견을 잘 받아들일 수 있게 되었습니다.
서로 중요하게 생각하는 포인트가 다르다 보니, PR 리뷰나 회의 과정에서 나 혼자였다면 지나쳤을 부분을 다른 관점에서 다시 돌아보게 되는 경험이 많았습니다. 이러한 과정에서 서로에게 배우고, 더 나은 방향을 함께 고민할 수 있었던 점이 협업의 가장 큰 가치라고 느꼈습니다.
일정이 빠듯하거나 구현이 잘 되지 않아 힘든 순간도 있었지만, 팀원 모두가 끝까지 책임감을 가지고 본인의 역할을 해주었고, 덕분에 프로젝트를 무사히 마무리할 수 있었습니다.
이번 프로젝트를 통해 의견을 조율하고, 기준을 함께 세우며, 다른 사람의 관점을 통해 더 나은 방향을 찾아가는 협업의 의미를 배울 수 있었습니다.