상태 관리와 인터랙션 설계
이번 채널에서는 HarmonyOS NEXT를 활용해 실시간으로 반응하는 음악 플레이어의 핵심 기능을 구현합니다. 이전 편에서 기본적인 레이아웃 구성과 컴포넌트 배치를 마쳤다면, 이번엔 사용자 인터랙션과 상태 동기화를 중심으로 심화된 개발 기법을 적용합니다.
핵심 상태 변수 정의
음악 플레이어의 동작은 세 가지 주요 상태로 구성됩니다:
@Component
export struct AudioController {
@State isPlaying: boolean = false;
@State elapsed: number = 0;
@State duration: number = 240; // 4분
// 컴포넌트 본문
}
isPlaying: 재생 중 여부를 나타내며, 버튼 아이콘 변경에 영향을 줍니다.elapsed: 현재 재생 시간(초 단위)을 저장하며, 진행률 표시 및 타임 스탬프에 사용됩니다.duration: 전체 음악 길이를 의미하며, 슬라이더 최대값 및 총 시간 표시에 활용됩니다.
모든 상태는 @State 데코레이터로 선언되어 있어, 값 변화 시 자동으로 뷰가 리렌더링됩니다.
재생 상태 제어
플레이/일시정지 버튼은 현재 상태에 따라 다르게 표시되며, 클릭 시 상태 전환을 수행합니다:
Button(this.isPlaying ? $r('app.media.play') : $r('app.media.pause'))
.width(48)
.height(48)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.isPlaying = !this.isPlaying;
})
이 코드는 다음과 같은 동작을 수행합니다:
isPlaying값에 따라 적절한 이미지를 선택하여 렌더링- 버튼 클릭 시 상태를 반전하고, 뷰가 즉시 갱신됨
진행률 조절
진행률 슬라이더는 Slider 컴포넌트로 구현되며, 현재 시간 상태와 연결됩니다:
Slider({
value: this.elapsed,
min: 0,
max: this.duration,
style: SliderStyle.OutSet
})
.onChange((value: number) => {
this.elapsed = Math.round(value);
})
.width('75%')
이 구조는 다음과 같이 작동합니다:
- 슬라이더의 초기 위치는
elapsed값에 따라 결정됨 - 사용자가 드래그하면
onChange이벤트 발생 → 새로운 시간 값으로 상태 업데이트 - 시간이 변경되면 슬라이더 위치와 텍스트 표시가 자동 반영됨
시간 포맷팅 처리
초 단위를 분:초 형식으로 변환하기 위해 유틸리티 함수를 정의합니다:
private formatDuration(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs < 10 ? '0' + secs : secs}`;
}
이 함수는 다음과 같은 로직을 따릅니다:
- 분 계산: 초를 60으로 나눈 몫
- 초 계산: 나머지 값
- 단수 초는 앞에 0 추가
- 결과 예:
3:05
UI에서 해당 함수를 호출하여 현재 및 전체 시간을 표시합니다:
Text(this.formatDuration(this.elapsed))
.fontSize(14)
.width(60)
Text(this.formatDuration(this.duration))
.fontSize(14)
.width(60)
고급 레이아웃 기법
음악 플레이어는 다층적 구조로 설계되어 있으며, 각 영역의 역할이 명확합니다:
Column (최상위 컨테이너)
├── Text (제목)
└── RowSplit (메인 영역)
├── Column (커버 이미지 영역)
│ ├── Image (앨범 아트)
│ ├── Text (곡 제목)
│ ├── Text (아티스트)
│ └── Row (관심/공유 버튼)
│ ├── Button (좋아요)
│ └── Button (공유)
└── Column (재생 컨트롤 영역)
├── Row (진행률)
│ ├── Text (현재 시간)
│ ├── Slider (진행 슬라이더)
│ └── Text (총 시간)
├── Row (재생 컨트롤)
│ ├── Button (이전 곡)
│ ├── Button (재생/일시정지)
│ └── Button (다음 곡)
└── Row (볼륨 조절)
├── Button (볼륨 아이콘)
├── Slider (볼륨 슬라이더)
└── Button (설정 옵션)
크기 및 정렬 설정
다양한 크기 지정 방식을 활용해 다양한 디바이스 환경에서도 일관된 레이아웃을 유지합니다:
- 퍼센트 기반:
height('60%')→ 부모 요소의 60% 높이 - 고정 크기:
width(200),height(200)→ 200px 고정 - 비율 기반:
width('75%')→ 부모 너비의 75%
이러한 혼합 사용은 반응형 디자인의 핵심입니다.
정렬 및 여백 관리
시각적 균형을 위해 다음 요소들을 활용했습니다:
justifyContent(FlexAlign.Center): 컨텐츠 수직 정렬margin({ top: 10, bottom: 10 }): 컴포넌트 간 여백 조절padding(16): 내부 여백 확보
인터랙션 최적화 전략
사용자에게 즉각적인 피드백을 제공하는 것이 중요합니다:
- 버튼 아이콘 변화 → 현재 재생 상태 반영
- 슬라이더 위치 변화 → 실제 재생 위치 반영
- 시간 텍스트 갱신 → 실시간 정보 제공
실제 애플리케이션에서는 이러한 변화가 내부 미디어 엔진과 연동되어야 하며, 예를 들어 onProgressUpdate 이벤트를 통해 자동으로 elapsed 값을 업데이트할 수 있습니다.
확장 기능 제안
다음과 같은 기능을 추가하여 사용자 경험을 더욱 풍부하게 만들 수 있습니다:
재생 모드 전환
@State playbackMode: string = 'sequential'; // 'sequential', 'random', 'repeat'
// UI에 버튼 추가
Button(this.getModeIcon())
.onClick(() => this.togglePlaybackMode());
private getModeIcon(): Resource {
switch (this.playbackMode) {
case 'sequential': return $r('app.icon.sequential');
case 'random': return $r('app.icon.random');
case 'repeat': return $r('app.icon.repeat');
default: return $r('app.icon.sequential');
}
}
private togglePlaybackMode(): void {
const modes = ['sequential', 'random', 'repeat'];
const currentIndex = modes.indexOf(this.playbackMode);
const nextIndex = (currentIndex + 1) % modes.length;
this.playbackMode = modes[nextIndex];
}
가사 표시 기능
interface LyricEntry {
time: number; // 초 단위
text: string;
}
@State lyrics: LyricEntry[] = [
{ time: 0, text: '햇살이 땅을 비추고' },
{ time: 15, text: '바람이 내 얼굴을 스친다' },
// ... 더 많은 가사
];
@State currentLyric: string = '';
// 가사 업데이트 로직
private updateCurrentLyric(currentTime: number): void {
for (let i = this.lyrics.length - 1; i >= 0; i--) {
if (currentTime >= this.lyrics[i].time) {
this.currentLyric = this.lyrics[i].text;
break;
}
}
}
// UI에 가사 표시
Text(this.currentLyric)
.fontSize(16)
.textAlign(TextAlign.Center)
.width('100%')
결론
본 문서에서는 HarmonyOS NEXT 기반 음악 플레이어의 상태 관리 및 인터랙션 설계를 심화적으로 다룹니다. @State를 통한 상태 선언, 이벤트 기반 인터랙션, 시간 포맷팅, 그리고 확장 가능한 아키텍처를 통해 실시간 반응형 애플리케이션 개발의 핵심 원리를 익혔습니다.