스트리밍 서비스 웹페이지 구현

mtak·2022년 6월 4일
0

미디어 웹 서비스 개발 과정

해당 프로젝트 참여 이유

web에 대한 이해 없이 spring으로 rest api만 만들왔다.
게시판 개발 과정에서 "여러개의 client에서 데이터를 받아서 하나의 시퀀스로 처리하는 서버 로직을 만들어봐라. 즉, input이 여러개인 아키테쳐를 설계해봐라" 라는 요구사항이 들어왔을 때 기술적으로 어떻게 그것이 가능한지 몰랐다.
직접 만들어보면서 브라우저의 동작 매커니즘을 이해하기 위해 본 프로젝트에 참여했다.

프로젝트 운영 매커니즘

3명의 42서울 카뎃이 개포 클러스터에 모여서 매일 2시간동안 학습 및 코딩을 했다.
90분동안 자율 개발하고 30분 동안 서로의 코드를 보면서 리뷰했다.
github의 이슈 관리 툴을 사용하여 서로의 진행상황과 좋은 레퍼런스를 공유했다.

개발 기간

2022.05.09 ~ 2022.06.02

개발 내용

mission1에서는 영상 콘텐츠의 메인 화면을 구현한다.
레이아웃,다양한 형태의 팝업(layer popup, alert, popup etc),이벤트 핸들링, 브라우저에서 히스토리를 남기는 매커니즘, DOM tree 조작, browser Storage 등에 대해 배워갈 수 있다.

유튜브 화면 비스무리하게 만들어지는 여정 시이~~작

1. 유튜브 메인 와꾸를 만들어보자

1-1. 레이아웃을 정해서 글이랑 그림을 올려볼까?

코드 보기
  <!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html;">
	<meta charset="UTF-8" />
	<style>
		.container {
			display: flex;
			flex-direction: row;
			justify-content: space-between;
			justify-content: center;
		}
		img {

		}

		figure {
			width: 400px;
			height: 400px;
			background-color: yellow;
			margin-top: 100px;
			margin-bottom: 100px;
			margin-left: 100px;
			margin-right: 100px;
		}

	</style>
</head>
<body>
	<header>
		
	</header>
	<main>
		<section class="container">
			<figure>
				<img src="../images/abc.jpeg"  title="설명글" alt="flowers" width="350" height="263">
				<figcaption>[ 그림 1. 위의 그림은 이쁜 꽃이네요! ]</figcaption>
			</figure>
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers" width="350" height="263">
				<figcaption>[ 그림 1. 위의 그림은 이쁜 꽃이네요! ]</figcaption>
			</figure>
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers" width="350" height="263">
				<figcaption>[ 그림 1. 위의 그림은 이쁜 꽃이네요! ]</figcaption>
			</figure>
		</section>

		<section class="container">
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers" width="350" height="263">
				<figcaption>[ 그림 1. 위의 그림은 이쁜 꽃이네요! ]</figcaption>
			</figure>
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers" width="350" height="263">
				<figcaption>[ 그림 1. 위의 그림은 이쁜 꽃이네요! ]</figcaption>
			</figure>
			<figure>
				<img src="../images/abc.jpeg"  title="설명글" alt="flowers" width="350" height="263">
				<figcaption>[ 그림 1. 위의 그림은 이쁜 꽃이네요! ]</figcaption>
			</figure>
		</section>

		<section class="container">
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers" width="350" height="263">
				<figcaption>[ 그림 1. 위의 그림은 이쁜 꽃이네요! ]</figcaption>
			</figure>
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers" width="350" height="263">
				<figcaption>[ 그림 1. 위의 그림은 이쁜 꽃이네요! ]</figcaption>
			</figure>
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers" width="350" height="263">
				<figcaption>[ 그림 1. 위의 그림은 이쁜 꽃이네요! ]</figcaption>
			</figure>
		</section>
		

	</main>
	<footer>

	</footer>
</body>
</html>

무식하게 다 때려넣어버리기 신공

1-2. 제목 한 줄이랑 내용 두줄정도 뿌려보자

자문:두줄 출력.. 그거 어케하는거죠?
자답: line-height 설정하고 그 값 * [출력하고 싶은 줄 수] 에 해당하는 값을 height에 넣으면 되는구나!!

코드 보기
<!DOCTYPE html>
<html>

<head>
	<meta http-equiv="Content-Type" content="text/html;">
	<meta charset="UTF-8" />
	<style>
		.container {
			display: flex;
			flex-direction: row;
			flex-wrap: wrap;
			justify-content: space-evenly;
		}

		figure {
			width: 20vw;
			height: 30vh;
			flex-basis: auto;
			margin-top: 1rem;
			margin-bottom: 1rem;
			margin-left: 1rem;
			margin-right: 1rem;
		}
		
		img {
			width: 100%;
			height: 80%;
		}

		figcaption {
			width: 100%;
			height: 20%;
			background-color: yellow;
			font-size: 1rem;
		}

		div.video-title {
			font-size: 100%;
			width: 80%;
			height: 30%;
			text-overflow: ellipsis;
			overflow     : hidden; 
			white-space  : nowrap;
			margin-bottom: 0rem;
			margin-top: 0rem;
		}
		div.video-metadata {
			font-size: 80%;
			width: 90%;
			display: inline-block;
			overflow     : hidden; 
			text-overflow: ellipsis;
			white-space: normal;
			line-height: 1.2;
			height:2.4em;
			text-align: left;
			word-wrap: break-word;
			display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
		}
	</style>
</head>

<body>
	<header>

	</header>
	<main>
		<section class="container">
			<figure>
					<img src="../images/abc.jpeg" title="설명글" alt="flowers">
					<figcaption>
						<div class="video-title">This is title This is titleThis is titleThis is titleThis is title</div>
						<div class="video-metadata">
							This is first line .This is first line .This is first line .This is first line .This is first line .This is first line .This is first line .This is first line .This is first line .This is first line .
						</div>
					</figcaption>
			</figure>
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers">
				<figcaption>
					<div class="video-title">This is title</div>
					<div class="video-metadata">
						This is first line .This is first line .This is first line .This is first line .This is first line .
						This is first line .This is first line .This is first line .This is first line .This is first line .
					</div>
				</figcaption>
		</figure>
		<figure>
			<img src="../images/abc.jpeg" title="설명글" alt="flowers">
			<figcaption>
				<div class="video-title">This is title</div>
				<div class="video-metadata">
					This is first line .This is first line .This is first line .This is first line .This is first line .
					This is first line .This is first line .This is first line .This is first line .This is first line .
				</div>
			</figcaption>
	</figure>
		</section>

		<section class="container">
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers">
				<figcaption>
					<div class="video-title">This is title</div>
					<div class="video-metadata">
						This is first line .This is first line .This is first line .This is first line .This is first line .
						This is first line .This is first line .This is first line .This is first line .This is first line .
					</div>
				</figcaption>
		</figure>
		<figure>
			<img src="../images/abc.jpeg" title="설명글" alt="flowers">
			<figcaption>
				<div class="video-title">This is title</div>
				<div class="video-metadata">
					This is first line .This is first line .This is first line .This is first line .This is first line .
					This is first line .This is first line .This is first line .This is first line .This is first line .
				</div>
			</figcaption>
	</figure>
	<figure>
		<img src="../images/abc.jpeg" title="설명글" alt="flowers">
		<figcaption>
			<div class="video-title">This is title</div>
			<div class="video-metadata">
				This is first line .This is first line .This is first line .This is first line .This is first line .
				This is first line .This is first line .This is first line .This is first line .This is first line .
			</div>
		</figcaption>
</figure>
		</section>

		<section class="container">
			<figure>
				<img src="../images/abc.jpeg" title="설명글" alt="flowers">
				<figcaption>
					<div class="video-title">This is title</div>
					<div class="video-metadata">
						This is first line .This is first line .This is first line .This is first line .This is first line .
						This is first line .This is first line .This is first line .This is first line .This is first line .
					</div>
				</figcaption>
		</figure>
		<figure>
			<img src="../images/abc.jpeg" title="설명글" alt="flowers">
			<figcaption>
				<div class="video-title">This is title</div>
				<div class="video-metadata">
					This is first line .This is first line .This is first line .This is first line .This is first line .
					This is first line .This is first line .This is first line .This is first line .This is first line .
				</div>
			</figcaption>
	</figure>
	<figure>
		<img src="../images/abc.jpeg" title="설명글" alt="flowers">
		<figcaption>
			<div class="video-title">This is title</div>
			<div class="video-metadata">
				This is first line .This is first line .This is first line .This is first line .This is first line .
				This is first line .This is first line .This is first line .This is first line .This is first line .
			</div>
		</figcaption>
</figure>
		</section>


	</main>
	<footer>

	</footer>
</body>

</html>

두줄 출력하는데 참고한 레퍼런스 => http://www.kunwi.co.kr/style/584

1-3. 팝업의 모든 것(과장 주의)

  • alert 창

    코드 보기
    let video_title = document.querySelector('.video-title');
    video_title.onclick = () => alert(video_title.textContent);
  • 팝업 창

    코드 보기
    function video_popup(e){
    	let win= window.open("", "_blank", "toolbar=yes,scrollbars=yes,resizable=yes,top=500,left=500,width=400,height=400");
    	win.document.write(`<h1 id="video-title">${e.target.textContent}</h1>\`);
    }
    let video_titles = document.querySelectorAll('.video-title');
    for (let i = 0; i < video_titles.length; i++) {
    	video_titles[i].addEventListener('click', video_layer_popup);
    }
  • 레이어 팝업

    코드 보기
    
    #popup_bg {
    	display: none;
    	position: absolute;
    	top:0;
      left:0;
      width:100%;
      height:100%;
      background:rgba(0,0,0,0.5);
    }
    #popup_bg > #popup {
    	position:absolute; 
      padding:15px;
      box-sizing:border-box;
      border-radius:15px;
      top:50%;
      left:50%;
      transform:translate(-50%, -50%); 
      width:600px;
      height:400px;
      background:#fff;
      box-shadow: 7px 7px 5px rgba(0,0,0,0.2); 
    }
    #popup_bg > #popup > h2 {
      margin-bottom:25px;
    }
    .popup-close {
    	float: right;
    }
    function video_layer_popup(e) {
    	let title = e.target.textContent;
    	let layer_popup = document.querySelector('#popup_bg');
    	let popup_title = document.querySelector('#popup-title');
    	popup_title.innerHTML = title;
    	layer_popup.style.display = 'block';
    }
    let video_titles = document.querySelectorAll('.video-title');
    for (let i = 0; i < video_titles.length; i++) {
    	video_titles[i].addEventListener('click', video_layer_popup);
    }
    let layer_popup_close = document.querySelector('.popup-close');
    layer_popup_close.addEventListener('click', () => {
    	let layer_popup = document.querySelector('#popup_bg');
    	layer_popup.style.display = 'none';
    });
    		<div id="popup_bg">
    			<div id="popup">
    				<h2 id="popup-title">This is mtak layer
    				</h2>
    				<button type="button" class="popup-close">X</button>
    			</div>
    		</div>

1-4. 상세 페이지로 전환을 해보자

코드 보기
//방법1개 - html 이용
<a href="http://127.0.0.1:5500/sub4/detail.html">
						<img src="http://127.0.0.1:5500/images/first.webp" title="설명글" alt="flowers">
</a>
//방법4개 - main.js
function goDetail(e) {
	let fig = e.target.closest('figure');
	localStorage.setItem('main-video-stream', e.target.src);
	localStorage.setItem('main-video-title', fig.querySelector('.video-title').textContent);
	localStorage.setItem('main-video-content', fig.querySelector('.video-metadata').textContent);

	// localStorage.setItem('main-video-title', e.target.closet('figure'))
	// location.assign("http://127.0.0.1:5500/sub4/detail.html");
	location.href = "http://127.0.0.1:5500/sub4/detail.html";
	// location.replace("http://127.0.0.1:5500/sub4/detail.html");
}
let video = document.querySelector('img');
for (let i = 0; i < video.length; i++) {
	video[i].addEventListener('click', goDetail());
}
    //detail.js
   let main_video_stream = document.querySelector(".main-video-stream");
let img = document.createElement("img");
img.src = localStorage.getItem("main-video-stream");
img.style.width = "100%";
img.style.height = "100%";
main_video_stream.appendChild(img);
let main_video_title = document.querySelector(".main-video-title");
let main_video_content = document.querySelector(".main-video-content");
main_video_title.textContent = localStorage.getItem("main-video-title");
main_video_content.textContent = localStorage.getItem("main-video-content");
//detail.html
<body>
	<div class="main-video">
		<div class="main-video-stream"></div>
		<div class="main-video-description">
			<div class="main-video-title"></div>
			<div class="main-video-content"></div>
		</div>
	</div>

</body>

1-5. 더보기 기능 구현

코드 보기

.main-video-content {
	font-size: 80%;
	width: 90%;
	display: inline-block;
	text-align: left;
	word-wrap: break-word;
	white-space: normal;
}

.showstep1 {
	overflow     : hidden; 
	text-overflow: ellipsis;
	line-height: 1.2;
	max-height: 4.8em;
	display: -webkit-box;
	-webkit-line-clamp: 4;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

.content{
	white-space: normal;
	line-height: 1.2;
	word-wrap: break-word;
	background-color: powderblue;
}
.hide{
	display: none;
}
//방법1 - hidden
document.addEventListener('DOMContentLoaded', function () { //DOM 생성 후 이벤트 리스너 등록
  // 더보기 버튼 이벤트 리스너
  document.querySelector('.btn_open').addEventListener('click', function (e) {

      let classList = document.querySelector('.main-video-content').classList; // 더보기 프레임의 클래스 정보 얻기
      if (classList.contains('showstep1')) {
          classList.remove('showstep1');
          document.querySelector('.btn_open').classList.add('hide');
      }
      //전체보기시 더보기 버튼 감추기 & 감추기 버튼 표시
      if (!classList.contains('showstep1') && !classList.contains('showstep2')) {
          e.target.classList.add('hide');
          document.querySelector('.btn_close').classList.remove('hide');
      }

      if (classList.contains('showstep2') && !classList.contains('showstep1')) {
          document.querySelector('.main-video-content').innerHTML = localStorage.getItem("main-video-content");
      }

  });
});

document.querySelector('.btn_close').addEventListener('click', function (e) {
  e.target.classList.add('hide');
  document.querySelector('.btn_open').classList.remove('hide');
  document.querySelector('.main-video-content').classList.add('showstep1'); // 초기 감춤 상태로 복귀
});

//컨텐츠 로딩 완료 후 높이 기준으로 클래스 재처리
window.addEventListener('load', function () {
  let contentHeight = document.querySelector('.main-video-content > .content').offsetHeight; //컨텐츠 높이 얻기
  if (contentHeight <= this.document.querySelector('.main-video-content > .content').style.lineHeight * 4) {
      document.querySelector('.main-video-content').classList.remove('showstep1'); // 초기값보다 작으면 전체 컨텐츠 표시
      document.querySelector('.btn_open').classList.add('hide'); // 버튼 감춤
  }
});
document.addEventListener('DOMContentLoaded', function () { //DOM 생성 후 이벤트 리스너 등록
  // 더보기 버튼 이벤트 리스너
document.querySelector('.btn_open').addEventListener('click', function (e) {

let classList = document.querySelector('.main-video-content').classList; // 더보기 프레임의 클래스 정보 얻기
...
      if (classList.contains('showstep2') && !classList.contains('showstep1')) {
          document.querySelector('.main-video-content > .content').innerHTML = localStorage.getItem("main-video-content");
          e.target.classList.add('hide');
          document.querySelector('.btn_close').classList.remove('hide');
      }
  });
	
//컨텐츠 로딩 완료 후 높이 기준으로 클래스 재처리
window.addEventListener('load', function () {
  let contentHeight = document.querySelector('.main-video-content > .content').offsetHeight; //컨텐츠 높이 얻기
  if (contentHeight <= this.document.querySelector('.main-video-content > .content').style.lineHeight * 4) {
      document.querySelector('.main-video-content').classList.remove('showstep1'); // 초기값보다 작으면 전체 컨텐츠 표시
      document.querySelector('.btn_open').classList.add('hide'); // 버튼 감춤
  } else {
      let main_video_content_short = localStorage.getItem("main-video-content").substring(0, 800) + "...";
      document.querySelector('.main-video-content > .content').innerHTML = main_video_content_short;
  }
});

2. 상세 페이지에 플레이리스트를 만들어보자

fetch()로 데이터 가져오는 법, video 태그 사용법, youtube api 사용법 등을 배워갈 수 있다.

2-1. 플레이 리스트를 만들고 거기에 플레이할 영상의 기본 정보 추가해보자

코드 보기
  let playlist = document.querySelector(".playlist");
	fetch("./data.json")
		.then(response => response.json())
		.then(data => {
			console.log(data);
			data = data.playlist;
			data.forEach(item => {
				console.log(item);
				let sub_video = document.createElement('figure');
				sub_video.className = "sub-video";
				let sub_img = document.createElement('img');
				sub_img.src = item.src;
				sub_video.appendChild(sub_img);
				let sub_desc = document.createElement('figcaption');
				let sub_desc_title = document.createElement('div');
				sub_desc_title.className = "sub-video-desc-title";
				sub_desc_title.textContent = item.title;
				sub_desc.appendChild(sub_desc_title);
				// let sub_video_desc_content = document.createElement('div');
				// sub_desc_content.className = "sub-video-desc-content";
				// sub_desc_content.textContent = item.content;
				// sub_desc.appendChild(sub_video_desc_content);
				sub_video.appendChild(sub_desc);
				playlist.appendChild(sub_video);
			});
		});
  {
	"playlist": [
		{
			"src": "http://127.0.0.1:5500/images/first.webp",
			"title": " This is title1",
			"content": "This is content1"
		},
		{
			"src": "http://127.0.0.1:5500/images/first.webp",
			"title": " This is title1",
			"content": "This is content1"
		},
		{
			"src": "http://127.0.0.1:5500/images/first.webp",
			"title": " This is title1",
			"content": "This is content1"
		},
		{
			"src": "http://127.0.0.1:5500/images/first.webp",
			"title": " This is title1",
			"content": "This is content1"
		},
		{
			"src": "http://127.0.0.1:5500/images/first.webp",
			"title": " This is title1",
			"content": "This is content1"
		},
		{
			"src": "http://127.0.0.1:5500/images/first.webp",
			"title": " This is title1",
			"content": "This is content1"
		},
		{
			"src": "http://127.0.0.1:5500/images/first.webp",
			"title": " This is title1",
			"content": "This is content1"
		}
	]
	
}

2-2. 재생목록을 만들고 순차적으로 플레이 되도록 해보자!

코드 보기
  //detail.js
  var player = videojs("player");

player.playlist([
    {
        sources: [{
            src: 'https://www.youtube.com/watch?v=AZGcmvrTX9M',
            type: 'video/youtube'
        }]
    },
    {
        sources: [{
            src: 'https://www.youtube.com/watch?v=n4YXauObskA',
            type: 'video/youtube'
        }]
    },
    {
        sources: [{
            src: 'http://media.w3.org/2010/05/sintel/trailer.mp4',
            type: 'video/mp4'
        }],
        poster: 'http://media.w3.org/2010/05/sintel/poster.png'
    }
]);

// Play through the playlist automatically.
player.playlist.autoadvance(0);
player.playlist.repeat(true);
  //detail.html
  <video id="player" class="video-js" controls autoplay muted preload="auto" width="854" height="480">
		<p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that
			<a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
	</video>
[아재가 서있는 이미지가 영상이다 ~~gif 매우 귀찮음~~]

프로젝트 일정

  • 16일 동안 2시간씩 3명의 스터디원이 90분동안 학습 + 구현을 하고 30분 동안 오프라인 라이브 리뷰를 하는 방식으로 진행했다.
  • 위의 총 7개 스텝으로 이슈를 나눴고, 캘린더의 넘버링
    은 해당 이슈의 번호이다.

후기

  • 웹페이지를 만들면서 서버가 어떤 방식으로 데이터를 받을 수 있는지, 또 굳이 서버까지 갈 필요 없이 로컬 스토리지에 저장해서 주고받는 데이터 양을 최적화 할 수 없는지 생각해보게 되었다.
  • url 변경없이 프론트의 컴포넌트를 동적으로 변경하면서 동작하는 방식인 SPA도 media 쿼리를 사용하면 만들 수 있겠다는 생각을 했다.
  • 여러사람이 같은 주제로 코드를 짜다 보니 다양한 접근법과 더 풍부한 키워드를 얻을 수 있어서 학습에 효율적이었다.

전체 코드

profile
노는게 젤 조아. 친구들 모여라!!

0개의 댓글