프론트엔드에서도 알고리즘은 중요하다
Dino • Software Engineer / Web / Front
화상회의는 이제 우리 일상의 필수적인 부분이 되었습니다. 팀 미팅, 고객 상담, 원격 교육 등 다양한 상황에서 화상회의를 활용하고 있죠. 그런데 화상회의 애플리케이션을 사용하다 보면, 참여자들이 화면에 어떻게 배치되는지가 사용자 경험에 큰 영향을 미친다는 것을 느낄 수 있습니다.
참여자가 2명일 때는 간단합니다. 화면을 반으로 나누면 되니까요. 하지만 참여자가 3명, 5명, 10명으로 늘어나면 어떻게 배치해야 할까요? 게다가 누군가 화면을 공유하면 어떻게 해야 할까요? 데스크톱과 모바일에서는 어떻게 다르게 보여줘야 할까요?
Channel의 Meet 서비스를 개발하면서 우리는 이러한 문제들을 해결하기 위한 레이아웃 알고리즘을 설계하고 구현했습니다. 이 글에서는 그 과정을 공유하고자 합니다.
화상회의 레이아웃 시스템을 설계하기 위해 먼저 요구사항을 정리했습니다.
동적 참여자 수
참여자는 실시간으로 입장하거나 퇴장할 수 있습니다
1명부터 수십 명까지 다양한 참여자 수를 지원해야 합니다
참여자 수 변화에 따라 레이아웃이 자연스럽게 전환되어야 합니다
다양한 화면 환경
데스크톱, 태블릿, 모바일 등 다양한 디바이스
각 디바이스의 다양한 화면 비율 (16:9, 4:3, 21:9 등)
브라우저 창 크기 조절에 대한 반응형 대응
비디오 품질 제약
각 참여자 박스는 최소 크기를 보장해야 합니다 (얼굴 식별 가능한 크기)
비디오 비율 제약: 너무 가로로 길거나 세로로 긴 박스는 부자연스럽습니다
최소 비율: 4:3 (세로로 긴 경우)
최대 비율: 16:9 (가로로 긴 경우)
최소 높이: 160px
특수 시나리오
화면 공유: 공유 화면은 더 큰 공간을 차지해야 합니다
화면 공유 시에도 다른 참여자들을 작게나마 볼 수 있어야 합니다
화면 공유의 원본 비율을 최대한 유지해야 합니다
우리는 다음과 같은 우선순위로 레이아웃을 최적화하기로 했습니다:
화면에 표시되는 참여자 수 최대화: 가능한 한 많은 참여자를 한 화면에 보여줘야 합니다
각 참여자 박스의 면적 합 최대화: 같은 수의 참여자를 표시한다면, 각 박스가 클수록 좋습니다
빈 공간 최소화: 화면 공간을 효율적으로 활용해야 합니다
이러한 목표들은 때로 서로 상충될 수 있습니다. 예를 들어, 참여자가 7명일 때 3x3 그리드(9칸)를 사용하면 2칸이 비게 되지만, 2x4 그리드(8칸)를 사용하면 1칸만 비게 됩니다. 하지만 2x4 그리드에선 좌우에 빈공간이 많이 생길 수 있습니다. 이런 트레이드오프를 해결하는 것이 핵심 과제였습니다.
참여자 7명일 때 3x3 레이아웃
참여자 7명일 때 2x4 레이아웃
우리는 레이아웃을 계층적으로 표현하기로 했습니다. 트리 구조를 활용하면 복잡한 레이아웃을 재귀적으로 계산할 수 있기 때문입니다.
GroupNode: 실제 박스를 담는 노드
GroupNode는 실제 참여자 비디오 박스들을 담는 말단 노드입니다.
여기서 주목할 점은 minRatio와 maxRatio를 선언적으로 정의할 수 있다는 것입니다. 이는 화상회의의 다양한 콘텐츠 특성을 반영한 설계입니다.
왜 비율 범위를 유연하게 만들었을까?
화상회의에서 표시되는 콘텐츠는 크게 두 가지 특성이 있습니다:
비율이 고정되어야 하는 콘텐츠 (예: 화면 공유)
슬라이드, 문서, 디자인 화면 등은 원본 비율을 유지해야 합니다
비율이 변하면 콘텐츠가 왜곡되어 보입니다
예: 1920x1080 화면 공유 → minRatio = maxRatio = 16/9
비율이 어느 정도 변해도 괜찮은 콘텐츠 (예: 참여자 비디오)
사람 얼굴은 약간 가로로 넓거나 세로로 긴 박스에도 자연스럽게 표시됩니다
레이아웃 유연성을 위해 비율 범위를 허용합니다
예: 일반 참여자 → minRatio = 4/3, maxRatio = 16/9
이러한 설계 덕분에 동일한 알고리즘으로 다양한 시나리오를 처리할 수 있습니다. 화면 공유 시에는 minRatio = maxRatio로 설정하여 고정 비율을 강제하고, 일반 참여자 비디오는 범위를 허용하여 공간을 효율적으로 활용합니다.
FlexNode: 자식 노드를 담는 컨테이너
FlexNode는 두 개의 자식 노드를 가지며, 이들을 가로 또는 세로로 배치합니다.
LayoutNode는 GroupNode 또는 FlexNode가 될 수 있는 유니온 타입입니다. 이를 통해 트리의 각 노드가 말단 그룹이거나 또 다른 분할 노드가 될 수 있습니다.
이 구조의 장점은 복잡한 레이아웃을 간단한 규칙의 조합으로 표현할 수 있다는 것입니다. 예를 들어, 화면 공유 시나리오는 다음과 같이 표현됩니다:
FlexNode (전체 컨테이너)
├─ GroupNode (화면 공유)
└─ GroupNode (일반 참여자들)가장 기본이 되는 것은 동일한 특성을 가진 참여자들을 하나의 그룹으로 배치하는 문제입니다. 예를 들어, 화면 공유 없이 일반 참여자 7명만 있는 경우입니다.
GroupNode는 이 문제를 해결합니다. 주어진 컨테이너 크기 내에서 박스들을 최적으로 배치하는 행(row)과 열(column)의 조합을 찾아 최대 면적을 달성합니다.
알고리즘 전략
모든 가능한 그리드 조합을 시뮬레이션합니다. 참여자가 7명이라면:
7x1, 1x7 (단일 행 또는 열)
4x2, 2x4 (비대칭 그리드)
3x3 (7개만 사용, 2개 빈 공간)
각 조합마다:
박스 크기 계산: 컨테이너를 행과 열로 나누어 각 박스의 크기를 계산
비율 제약 적용: minRatio와 maxRatio 범위 내로 조정
제약 조건 검증: 최소 높이, 최소 너비, 컨테이너 범위 검증
면적 계산: 모든 박스의 총 면적을 계산하여 최적해 선택
이렇게 하면 주어진 공간을 가장 효율적으로 활용하는 레이아웃을 찾을 수 있습니다. 같은 면적이라면 빈 공간이 적은 레이아웃을 선택합니다.
마지막 행 최적화
참여자가 7명이고 3x3 그리드를 선택했다면, 마지막 행에는 1개의 박스만 배치됩니다. 이 경우 마지막 행의 박스는 maxRatio 제약을 유지하면서 가능한 한 넓게 표시하고, 중앙 정렬합니다.
참여자 7명일 때 마지막 행 최적화 - 마지막 행의 박스가 더 넓게 표시되는 모습
GroupNode로 단일 그룹의 최적 레이아웃을 찾을 수 있습니다. 하지만 화면 공유가 추가되면 상황이 달라집니다. 화면 공유는 일반 참여자와 다른 특성을 가집니다:
원본 비율을 유지해야 함 (minRatio = maxRatio = 16/9)
더 큰 공간을 차지해야 함 (예: 75%)
일반 참여자와는 별도로 배치되어야 함
이때 FlexNode를 사용합니다. FlexNode는 두 개의 자식 그룹을 가지며, 이들을 가로 또는 세로로 배치할지 결정합니다.
트랙 그룹핑
먼저 비디오 트랙들을 특성별로 그룹으로 나눕니다:
1. 화면 공유 트랙 분리: 화면 공유 스트림을 찾아 별도 처리
2. 원본 비율 추출: 화면 공유의 실제 해상도(예: 1920x1080)를 가져와 비율 계산
3. 노드 생성:
- Primary Node (화면 공유): minRatio = maxRatio = 원본 비율로 고정
- Tertiary Node (일반 참여자): minRatio = 4/3, maxRatio = 16/9로 유연하게
최종 트리 구조:
FlexNode (ratio: 0.75)
├─ GroupNode (화면 공유 1개) <- 75% 공간
└─ GroupNode (일반 참여자 5명) <- 25% 공간횡/종 배치 검증과 공간 최적화
FlexNode는 두 가지 배치 방향을 모두 시뮬레이션하며, 각 방향마다 공간을 효율적으로 활용하는 알고리즘이 동작합니다.
횡 배치 시뮬레이션:
첫 번째 자식에게 초기 할당: 전체 너비의 75%(설정 가능)를 할당
첫 번째 자식의 최적 레이아웃 계산: 할당받은 공간 내에서 최적 면적 계산
실제 사용 공간 확인: 첫 번째 자식이 실제로 사용한 너비(optWidth)를 확인
예: 1920px의 75% = 1440px를 할당했지만, 실제로는 1200px만 사용
두 번째 자식에게 남은 공간 할당: 전체 너비 - 첫 번째 자식의 실제 사용 너비
예: 1920px - 1200px = 720px (단순 25%인 480px보다 많음!)
종 배치 시뮬레이션:
같은 방식으로 높이를 기준으로 계산
이렇게 첫 번째 자식이 사용하지 않은 공간을 두 번째 자식에게 추가로 할당함으로써, 전체 공간을 더 효율적으로 활용할 수 있습니다. 특히 첫 번째 자식이 고정 비율(화면 공유 등)을 가질 때, 남은 공간을 두 번째 자식이 최대한 활용하게 됩니다.
각 방향의 시뮬레이션 후 두 자식의 총 박스 면적을 비교하여 더 큰 쪽을 선택합니다. 이 재귀적 구조 덕분에 컨테이너의 가로세로 비율에 따라 자동으로 최적의 배치 방향이 결정됩니다.
Flex 노드의 횡/종 배치 시뮬레이션 - 같은 컨텐츠가 가로로 넓은 화면에서는 횡 배치, 세로로 긴 화면에서는 종 배치되는 모습
재귀적 구조의 확장성
이 재귀적 설계는 더 복잡한 시나리오로도 확장할 수 있습니다. 예를 들어, 참여자 고정(Pinning) 기능을 구현할 때도 동일한 구조를 활용할 수 있습니다:
FlexNode (ratio: 0.75)
├─ FlexNode (ratio: 0.75) <- 75% 공간
│ ├─ GroupNode (화면 공유 1개)
│ └─ GroupNode (고정 발표자 2명)
└─ GroupNode (일반 참여자 5명) <- 25% 공간이 구조에서는:
최상위 FlexNode가 공간을 75:25로 분할
왼쪽 75% 공간을 다시 FlexNode로 분할하여 화면 공유와 고정 발표자 배치
오른쪽 25% 공간에 일반 참여자들 배치
이렇게 FlexNode를 중첩하면, 화면 공유와 고정 발표자를 강조하면서도 일반 참여자를 함께 표시할 수 있습니다. 각 레벨에서 횡/종 배치 검증이 재귀적으로 수행되어 항상 최적의 레이아웃을 찾게 됩니다.
중첩된 FlexNode 구조 - 발표자 고정 + 화면 공유 + 일반 참여자
알고리즘이 실제로 어떻게 동작하는지 몇 가지 케이스를 살펴보겠습니다.
입력
컨테이너: 1280x720 (16:9)
참여자: 3명
제약: minRatio=4/3, maxRatio=16/9, minHeight=160
시뮬레이션 과정
3명을 표시하는 경우:
1x3: width=426, height=720 -> ratio=0.59 < 4/3 ❌ (너무 세로로 김)
3x1: width=1280, height=240 -> ratio=5.33 > 16/9 ❌ (너무 가로로 김)
2x2: width=640, height=360 -> ratio=1.78 (16/9) ✅
area = 640 * 360 * 3 = 691,200
최적 레이아웃: 2x2 (1칸 빈 공간)결과적으로 2x2 그리드가 선택되며, 마지막 행에는 1개의 박스만 배치됩니다. 마지막 행의 박스는 더 넓게 표시되어 중앙 정렬됩니다.
참여자 3명 레이아웃 - 2x2 그리드에서 마지막 박스가 중앙에 크게 표시
입력
컨테이너: 1920x1080
참여자: 5명 (1명이 1920x1080 화면 공유)
ratio: 0.75 (화면 공유가 75% 차지)
트리 구조 생성
FlexNode (ratio: 0.75)
├─ GroupNode (화면 공유 1개, 비율 고정 16/9)
└─ GroupNode (일반 참여자 4명)레이아웃 계산
횡 배치 시뮬레이션:
첫 번째 자식 (화면 공유): 1440x1080
-> 1920x1080 비율(16/9) 유지하면서 1440x1080 공간에 맞춤
-> 실제 크기: 1440x810 (중앙 정렬)
-> area = 1440 * 810 = 1,166,400
두 번째 자식 (4명): 480x1080
-> 2x2 그리드, 각 박스 240x270
-> area = 240 * 270 * 4 = 259,200
총 면적 = 1,425,600
종 배치 시뮬레이션:
첫 번째 자식 (화면 공유): 1920x810
-> 16/9 비율 유지하면서 1920x810 공간에 맞춤
-> 실제 크기: 1440x810 (중앙 정렬)
-> area = 1440 * 810 = 1,166,400
두 번째 자식 (4명): 1920x270
-> 4x1 그리드는 너무 가로로 김
-> 2x2 그리드 시도하지만 높이 부족
-> 면적 감소
총 면적 = 1,425,600보다 작음
결정: 횡 배치 선택최종적으로 화면 공유가 왼쪽에 크게 배치되고, 일반 참여자들이 오른쪽에 2x2 그리드로 배치됩니다.
화면 공유 + 참여자 5명 레이아웃 - 화면 공유가 왼쪽 75%를 차지하고, 참여자들이 오른쪽에 2x2로 배치
입력
컨테이너: 390x844 (모바일 세로 모드)
참여자: 4명
시뮬레이션 과정
4명을 표시하는 경우:
2x2: width=195, height=422 -> ratio=0.46 < 4/3 ❌
4x1: width=72.5, height=54.375
area = 72.5 * 54.375 * 4 ≈ 15,768
1x4: width=390, height=211 -> ratio=1.85 ✅
area = 390 * 211 * 4 = 329,160
최적 레이아웃: 1x4 (세로 스택)모바일의 좁고 긴 화면에서는 참여자들이 세로로 쌓이는 레이아웃이 선택됩니다.
모바일에서 참여자 4명 - 세로로 쌓인 레이아웃
박스 개수를 줄여가며 시뮬레이션할 때, 최적 결과를 찾으면 즉시 중단합니다. 대부분의 경우 모든 박스를 표시하는 레이아웃을 첫 번째 시도에서 찾을 수 있습니다.
React의 useMemo와 useCallback을 활용하여 불필요한 재계산을 방지합니다. 참여자 목록이나 컨테이너 크기가 변경될 때만 레이아웃을 재계산합니다.
최종 결과에서 Math.floor를 사용하여 정수로 변환합니다. 이는 브라우저의 렌더링 성능을 향상시키고, 서브픽셀 렌더링으로 인한 흐릿함을 방지합니다.
ResizeObserver를 사용하여 컨테이너 크기가 변경될 때만 레이아웃을 재계산합니다. 브라우저 창 크기 조절에 효율적으로 대응할 수 있습니다.
핵심 알고리즘에 대한 단위 테스트를 작성했습니다:
최적 레이아웃 계산 검증: 참여자 3명일 때 2x2 그리드가 선택되는지 확인
비율 제약 조건 검증: minRatio와 maxRatio가 올바르게 적용되는지 확인
최소 크기 제약 검증: minHeight를 만족하지 못하는 레이아웃은 제외되는지 확인
면적 최적화 검증: 가장 큰 면적을 가진 레이아웃이 선택되는지 확인
다양한 엣지 케이스를 테스트했습니다:
참여자 1명
단순히 전체 화면을 채움
비율 제약을 유지하며 중앙 정렬
매우 작은 컨테이너
최소 높이를 만족할 수 없는 경우 처리
표시할 수 있는 최대 참여자 수 계산
극단적인 화면 비율
21:9 울트라와이드 모니터
세로로 긴 모바일 화면
동적 참여자 변화
참여자 입장/퇴장 시 부드러운 전환
레이아웃 변경 시 애니메이션
일반적으로 프론트엔드 개발에서는 알고리즘이나 CS 지식을 크게 요구하지 않는다고 생각하는 경향이 있습니다. UI 컴포넌트를 만들고, API를 연동하고, 상태를 관리하는 것이 주된 업무라고 여겨지죠.
하지만 이번 레이아웃 알고리즘을 개발하면서 그 생각이 완전히 바뀌었습니다. 재귀(Recursion), 트리 구조(Tree), 분할 정복(Divide and Conquer), 동적 계획법적 사고(DP-like thinking), 그리디 알고리즘(Greedy) 등 CS의 핵심 개념들이 실제 프론트엔드 문제를 해결하는 데 직접적으로 활용되었습니다.
계층적 노드 구조는 트리 알고리즘의 응용
FlexNode의 공간 분할과 재귀적 계산은 분할 정복의 전형적인 패턴
횡/종 배치 검증은 재귀적 최적화의 실례
모든 그리드 조합 시뮬레이션은 완전 탐색의 실용적 활용
단순히 라이브러리를 사용하는 것을 넘어, 문제의 본질을 이해하고 직접 알고리즘을 설계하는 과정은 매우 흥미로웠고, 프론트엔드 개발자로서의 역량을 한 단계 끌어올릴 수 있었습니다.
화상회의 레이아웃 알고리즘을 개발하면서 많은 것을 배웠습니다. 단순해 보이는 "참여자를 화면에 배치하기"라는 문제도, 깊이 들어가면 다양한 제약 조건과 최적화 목표를 고려해야 하는 복잡한 문제였습니다.
하지만 문제를 잘 정의하고, 적절한 자료구조를 선택하고, 체계적으로 테스트하며 우아한 해결책을 찾을 수 있었습니다.
이 글이 유사한 문제를 해결하는 분들에게 도움이 되기를 바랍니다. 궁금한 점이 있으시다면 언제든 메일을 남겨주세요!
예시 코드는 아래 링크를 통해 확인하실 수 있습니다.
We Make a Future Classic Product