
HTTP(Hyper Text Transfer Protocol) 은 월드 와이드 웹 사이에서 통신할 수 있는 프로토콜이다. 주로 HTTP 문서를 주고받는 용도로 사용하며 TCP와 UDP 프로토콜을 사용한다.
HTTP 는 요청과 응답으로 구분할 수 있다.
서버/클라이언트 구조에서 요청을 보내는 쪽이 클라이언트, 요청을 받아서 처리하는 쪽이 서버이다.

protocol: HTTP 요청은 http 또는 https 프로토콜로 실행될 수 있다. https(security)는 보안이 더욱 강화된 프로토콜이다.
host : 요청하는 사이트의 도메인을 의미한다.
port.NO : 요청이 서버로 전달될 때 어떤 포트로 전달할지 지정한다.
resource Path: 요청의 경로이다. 불러올 데이터의 정보가 명시되어 있으며 경로에 따라 API 서버에서 로직을 실행하고 데이터를 반환한다.
query: 추가로 전송할 정보이다. ? 다음에 쿼리를 추가할 수 있으며 각 쿼리는 & 로 구분한다.
각 쿼리 데이터는 Map 형태처럼 키와 값으로 이루어져 있으며 '키' = '값' 형태이다.
헤더는 메타데이터 (데이터 바디가 어떻게 구성되어 있는지, 데이터의 총 길이는 어느 정도인지, 어떤 브라우저에서 보낸 요청인지)에 대한 메타 정보를 입력하는 부분이다.
또한 로그인 후 발급받은 토큰의 정보를 서버에 보낼 때도 헤더에 토큰 정보를 첨부한다.

프로그램이 다른 프로그램과 통신할 때, 정해진 규격의 API 를 사용한다.
REST(Representional State Transfer) API 는 REST 기준을 따르는 HTTP API 이다.
HTTP API 이므로 4개의 메소드를 똑같이 제공한다.
차이점은 REST API 는 균일한 인터페이스, 무상태, 계층화, 캐시 원칙을 준수하는 HTTP API 이다.
균일한 인터페이스: 요청은 균일한 인터페이스를 가지고 요청만으로도 어떤 리소스를 접근하려는지 파악되어야 한다.
만약 이 이상의 정보가 필요한 요청이라면 그 정보도 제공되어야 한다.
무상태: API가 상태를 갖도록 요구하지 않는다.
즉 어떤 요청이 이전이나 이후의 요청에 영향을 일절 미치지 않고 전부 독립적이며 임의의 순서로 처리될 수 있어야 한다.
계층화된 시스템: 클라이언트와 서버 사이에 다른 중개자에 요청 연결할 수 있다.
클라이언트에게는 보이지 않고, 이 중개자는 또 다른 서버일 수 있다.
캐시: 클라이언트의 체감 응답 속도를 개선할 목적으로 일부 리소스를 저장할 수 있어야 한다.
만약 공통으로 사용되는 이미지나 헤더가 있을 때 해당 요청을 캐싱함으로써 응답 속도를 올리거나 불필요한 요청을 줄일 수 있따.
아예 캐시가 불가능한 API를 지정할 수도 있다.
플러터 프레임워크에서 HTTP 요청을 하는 데 일반적으로 DIO 플러그인을 사용한다.
DIO 플러그인을 사용하면 손쉽게 HTTP 요청을 할 수 있다.
단순히 DIO 클래스를 인스턴스화 하고 메소드를 GET, POST, PUT, DLELTE함수 이름으로 실행해주면 된다.
모든 함수의 첫 매개변수에는 요청을 보내는 URL을 입력해야 한다.
HTTP 요청에서 body를 구성할 때 사용하는 구조는 크게 XML과 JSON 으로 나뉜다.
현대에는 대부분 신식인 JSON구조를 사용한다.
{
'name' : 'Code Factory',
'languages' : ['Javascript', 'Dart'],
'age' : 2
}
REST API 요청할 때 요청 및 응답 body에 JSON 구조를 자주 사용한다.
플러터에서 JSON 구조로 된 데이터를 응답받으면 직렬화(serialization)를 통해 클래스의 인스턴스로 변환하여 사용할 수 있다.
이번 프로젝트는 HomeScreen 하나만 사용한다.
최상단에 AppBar가 위치하고 있고 아래에 다수의 유튜브 동영상들이 리스트로 위치한다.

HTTP 요청의 응답을 담을 모델 클래스를 구현
UI 작성
DIO를 사용해서 직접 API 요청 진행
요청 재실행 후 리스트를 갱신할 수 있는 새로고침 기능 만들기
유튜브 API를 사용하면 엄청나게 많은 정보를 가져올 수 있지만 동영상 ID와 제목만 활용하겠다.
이러한 ID와 제목 정보를 담을 VideoModel 클래스를 구현해서 데이터를 관리하겠다.
class VideoModel {
final String id;
final String title;
VideoModel({
required this.id,
required this.title,
});
}
CustomYoutubePlayer 위젯은 유튜브 동영상을 재생할 수 있는 상태로 제공하는 역할을 한다.
youtube_player_flutter 플러그인을 사용해서 구현한다.
CustomYoutubePlayer 위젯은 VideoModel 타입의 videoModel 변수를 HomeScreen 으로부터 입력받고 이 정보를 기반으로 UI를 구현한다.
import 'package:cf_tube/model/video_model.dart';
import 'package:flutter/material.dart';
// 유튜브 재생기를 사용하기 위해 패키지 불러오기
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
// 유튜브 동영상 재생기가 될 위젯
class CustomYoutubePlayer extends StatefulWidget {
// 상위 위젯에서 입력받을 동영상 정보
final VideoModel videoModel;
const CustomYoutubePlayer({
super.key,
required this.videoModel,
});
@override
State<CustomYoutubePlayer> createState() => _CustomYoutubePlayerState();
}
class _CustomYoutubePlayerState extends State<CustomYoutubePlayer> {
@override
Widget build(BuildContext context) {
return Container();
}
}
class CustomYoutubePlayer extends StatefulWidget {
.
.
.
class _CustomYoutubePlayerState extends State<CustomYoutubePlayer> {
YoutubePlayerController? controller; // 컨트롤러 생성
@override
void initState() {
super.initState();
controller = YoutubePlayerController( // 1. 컨트롤러 선언
initialVideoId: widget.videoModel.id, // 처음 실행할 동영상의 ID
flags: YoutubePlayerFlags(
autoPlay: false, // 자동실행 사용 안하기
),
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
YoutubePlayer( // 유튜브 동영상을 재생할 수 있는 위젯
controller: controller!,
showVideoProgressIndicator: true, // 동영상 진행 상황을 알려주는 슬라이더 보여주기
),
const SizedBox(
height: 16.0,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
widget.videoModel.title, // 동영상 제목
style: TextStyle(
color: Colors.white,
fontSize: 16.0,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(
height: 16.0,
),
],
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
controller!.dispose(); // State 폐기 시 컨트롤러 또한 폐기
}
}
CustomYoutubePlayer 위젯에 매개변수로 샘플 동영상 ID와 제목을 입력하고 실행하면 동영상이 렌더링되는걸 볼 수 있다.
import 'package:cf_tube/component/custom_youtube_player.dart';
import 'package:cf_tube/model/video_model.dart';
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: CustomYoutubePlayer(
videoModel: VideoModel(
id: '3Ck42C2ZCb8',
title: '다트 언어 기본기 1시간만에 끝내기',
),
),
);
}
}

위에서 보았듯이 현재는 제공해준 동영상 ID를 기반으로 화면을 렌더링한다.
최신 영상 순서대로 동영상을 불러오도록 Dio패키지를 이용한 HTTP 요청을 구현해보자
const API_KEY= '발급받은 토큰입력';
// Youtube Data API V3 URL
const YOUTUBE_API_BASE_URL = 'https://youtube.googleapis.com/youtube/v3/search';
const CF_CHANNEL_ID = '코팩튜브 ID 입력'; // 코드팩토리 채널 ID
Youtube Data API v3는 많은 기능을 제공해 주는데 그중 Search:list API를 사용해서 특정 채널에서 다수의 동영상을 최신순으로 불러오겠다.
다음은 사용할 Search:list API의 정의이다.

결과값으로 받은 복잡한 JSON 구조를 필요한 형태인 List<VideoModel> 형태로 전환해줘야 한다.
import 'package:cf_tube/const/api.dart';
import 'package:cf_tube/model/video_model.dart';
import 'package:dio/dio.dart';
class YoutubeRepository {
// 비동기로 동영상 정보를 받아오는 getVideos()함수
static Future<List<VideoModel>> getVideos() async {
// GET 메소드 보내기
final response = await Dio().get(
YOUTUBE_API_BASE_URL, // 요청을 보낼 URL
queryParameters: { // 요청에 포함할 쿼리 변수들
'channelId': CF_CHANNEL_ID,
'maxResults': 50,
'key': API_KEY,
'part': 'snippet',
'order': 'date',
},
);
final listWithData = response.data['items'].where(
(item) =>
item?['id']?['videoId'] != null && item?['snippet']?['title'] != null,
); // videoId와 title 이 null 이 아닌 값들만 필터링
return listWithData
.map<VideoModel>(
(item) => VideoModel(
id: item['id']['videoId'],
title: item['snippet']['title'],
),
)
.tolist(); // 필터링된 값들을 기반으로 VideoModel 생성
}
}
받은 데이터를 기반으로 동영상을 리스트 형태로 구현하겠다.
ListView 위젯을 사용하여 여러 위젯을 리스트로 구현한다.
import 'package:cf_tube/component/custom_youtube_player.dart';
import 'package:cf_tube/model/video_model.dart';
import 'package:cf_tube/repository/youtube_repository.dart';
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
centerTitle: true, // 제목 가운데 정렬
title: Text(
'코팩튜브',
),
backgroundColor: Colors.black,
),
body: FutureBuilder<List<VideoModel>>(
future: YoutubeRepository.getVideos(), // 유튜브 영상 가져오기
builder: (context, snapshot) {
if (snapshot.hasError) { // 에러 표시
return Center(
child: Text(
snapshot.error.toString(),
),
);
}
if (!snapshot.hasData) { // 로딩중 표시
return Center(
child: CircularProgressIndicator(), // 로딩 위젯
);
}
return ListView( // List<VideoModel> 을 CustomYoutubePlayer 로 매핑
physics: BouncingScrollPhysics(), // 아래로 당겨서 스크롤할 때 튕기는 애니메이션 추가
children: snapshot.data!
?.map((e) => CustomYoutubePlayer(videoModel: e))
.toList() ??
[], // snapshot.data가 null이 아닌지 확인하는 조건을 추가
);
},
),
);
}
}

build() 함수에 FutureBuilder 를 사용하면 FutureBuilder 가 화면에 렌더링 될 때 마다 future 매개변수에 입력된 함수가 실행된다.
이를 이용해서 홈 스크린에서 리스트를 아래로 당기면 새로고침되는 기능을 추가하겠다.
.
.
.
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
centerTitle: true, // 제목 가운데 정렬
title: Text(
'코팩튜브',
),
backgroundColor: Colors.black,
),
body: FutureBuilder<List<VideoModel>>(
future: YoutubeRepository.getVideos(), // 유튜브 영상 가져오기
builder: (context, snapshot) {
.
.
.
return RefreshIndicator( // 새로고침 기능이 있는 위젯
child: ListView(
physics: BouncingScrollPhysics(), // 아래로 당겨서 스크롤할 때 튕기는 애니메이션 추가
children: snapshot.data!
?.map((e) => CustomYoutubePlayer(videoModel: e))
.toList() ??
[],
),
onRefresh: () async { // 새로고침 시 setState() 함수를 실행해서 build()함수가 재실행되게 한다.
setState(() {});
},
);
},
),
);
}
}
