Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1단계 - 나만의 유튜브 강의실] 도비(김정혁) 미션 제출합니다. #3

Merged
merged 73 commits into from
Mar 9, 2021

Conversation

zereight
Copy link

@zereight zereight commented Mar 4, 2021

안녕하세요. 리뷰어님!
서니(@sunhpark42) 와 함께 페어를 진행하여 리뷰를 받게된 도비입니다.

데모페이지

demo (3배속)

src/js/key.jsexport const MY_KEY = 'aaaaa'; 로 Youtube Data API v3 키를 입력하여 사용하실 수 있습니다!

🎯 step1 검색 기능

  • 유튜브 검색 API를 통해서, 내가 추가로 보고 싶은 영상들을 검색할 수 있다.
    • 검색 시 엔터키를 눌렀을 때와 마우스로 검색 버튼을 눌렀을 때 검색 동작이 이루어진다.
  • 로딩컴포넌트: 데이터를 불러오는 중일 때, 현재 데이터를 불러오는 중임을 skeleton UI로 보여준다.
  • 검색 결과가 없는 경우 결과 없음 이미지를 추가하여, 사용자에게 메시지를 보여준다.
    • 검색 결과 없음 이미지는 src/images/status/not_found.png 경로에 있다.
  • 최초 검색결과는 10개까지만 보여준다. 더 많은 데이터는 스크롤을 내릴 때 추가로 불러온다.
    • 검색 결과 화면에서 유저가 브라우저 스크롤 바를 끝까지 이동시켰을 경우, 그다음 10개 아이템을 추가로 api요청하여 불러온다.
  • 내가 검색한 영상들의 json 데이터를 저장할 수 있다. (실제 저장이 아닌 영상 id를 Web Storage에 저장). 단 이미 저장된 경우는 저장 버튼이 보이지 않게 한다.
  • 저장 가능한 최대 동영상의 갯수는 100개이다.
  • 검색 모달에 다시 접근했을 때 가장 마지막에 검색한 키워드로 검색한 결과를 보여준다.
  • 최근 검색 키워드를 3개까지 화면상에 검색창 하단에 보여준다.
    • 최근 검색 키워드를 클릭시, 해당 검색어로 검색된 결과를 보여준다.
    • 최근 검색어 내에 있는 키워드를 클릭하거나, 검색하면 (중복 시) 최근 검색어에서 가장 앞으로 이동 및 검색결과를 표시한다.
    • 최근 검색어를 local Storage에 저장한다.

질문 있어요!

  • innerHTML에서 이벤트를 줄 수 있는 방법이 없을까요..? element를 이벤트가 달린 상태로 생성하고 싶은데 appendChild로 하자니 코드가 너무 더러워지는것 같습니다. ㅜㅜ
  • 리덕스를 사용하여 상태관리가 되도록 하였는데요. 상태관리에 대한 피드백도 주시면 감사하겠습니다!

sunhpark42 and others added 30 commits March 2, 2021 15:00
Copy link

@ysm-dev ysm-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI 피드백

image
disable된 버튼은 hover스타일을 없애고 클릭될것같은 cursor: pointer 보다는 cursor: not-allowed 같은 cursor스타일을 줘도 좋을것같아요 👍

  • 영상을 재생 후에 팝업을 닫으면 백그라운드에서 계속 재생되어서 소리가 그대로 들리는데 이런부분은 어떻게처리해야할까요? 👀

  • 영상 아래의 텍스트 영역이 클릭이벤트가 걸려있지 않은데에도 불구하고 cusor가 pointer네요!

innerHTML에서 이벤트를 줄 수 있는 방법이 없을까요..? element를 이벤트가 달린 상태로 생성하고 싶은데 appendChild로 하자니 코드가 너무 더러워지는것 같습니다. ㅜㅜ

  • event attribute를 inline으로 줄수도있지만 callback 함수가 global scope에 존재해야해서 좋은 방법은 아닙니다.
const test = () => alert('test')
$dom.innerHTML = `<div class="test2" onclick="test()">test</div>`
  • 그리고 innerHTML도 보안이나 성능상 현업에서는 자주 사용하지 않습니다. (어떤 보안문제가있는지도 공부해보시면 좋을것같아요)
  • 실제로는 Vanila JS로 작성한다면 대부분의 경우 코드가 더러워지더라도 document.createElementappendChild 를 사용하는것이 안전합니다.
  • 이러한 한계때문에 Facebook에서는 React에서 사용되는 JSX라는 문법을 만들기도 했고, hyperscript, htm 이러한 헬퍼 함수들도 존재합니다.
  • 내용이 방대하니 '이런것도 있구나.' 정도로 알아두시고 나중에 JSX문법과 React를 배울때 좀더 들여다보시면 좋을것같습니다. 👍

리덕스를 사용하여 상태관리가 되도록 하였는데요. 상태관리에 대한 피드백도 주시면 감사하겠습니다!

  • 훌륭합니다! Redux의 API를 참고해서 자바스크립트로 구현하려고 많이 고민하신 흔적이 보이네요 👍
  • 상태가 변경되면 모든 컴포넌트에 render함수를 호출하고 각 render함수에서 상태가 변경되었는지 확인하고있는데,
    render함수에서 상태가 변경되었는지 조건문으로 확인하는게 아니라, 상태가 변경되었을 때만 render함수를 부르도록 만들면 코드가 좀더 간결해질것같네요! 지금은 render함수가 조금 복잡해보여요.

Comment on lines 5 to 8
this.key = {
videos: LOCALSTORAGE_KEYS.VIDEOS,
searchHistory: LOCALSTORAGE_KEYS.SEARCH_HISTORY,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalStorage라는 브라우저의 API를 사용하는 util 라이브러리처럼보여요!
util함수로 빼고 이러한 도메인과 관련된 값들은 도메인과 관련된 파일에서 상수로 관리하면 좋을것같아요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵넵! 페어도 같은 피드백을 받아서 localstorage관련 유틸로 따로 뺴주었습니다.

Comment on lines 26 to 28
setItem(key, array) {
localStorage.setItem(this.key[key], JSON.stringify(array));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

array만 받을수있는 함수 말고 어떤 값이든 받을 수 있도록(어떤값이든 localStorage에 저장될수 있도록 좀더 범용적으로 바꿀수있을까요?

  1. 만약 JSON.stringify함수에 직렬화 할 수 없는 값(ex. undefined, function, Symbol, cyclic object) 등이 들어오면 어떻게 될까요? 그런상황은 어떻게 예외처리 해야할까요?
  2. localStorage에 array로 저장하는것과 object로 저장하는것에 어떤차이가있을까요?

Copy link
Author

@zereight zereight Mar 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 예외상황을 처리하기위해서, cyclic object같은 경우 decycle되게 유틸에서 만들었고, undefined, function, Symbol타입의 값들은 JSON.stringify에서 undefined가 반환되므로, undefined가 아닐때만 값을 셋팅해주도록 수정하였습니다!
  2. 지금 videos는 object의 배열 ( [{}, {}, {}] )로, searchHistory는 string의 배열 ( ["..", "..", ".."] )으로 저장하고 있는데요!
    해당 key값이 가지고 있는 value가 여러개면 array형태로 저장하는것 아닌가요?.?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 키가 있는지 없는지 찾을때 객체로 저장할때와 배열로 저장할때 시간복잡도가 어떻게다를까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 무슨뜻인지 이해했습니다! 배열일때에는 최악의 경우 O(n)이 걸리나, videoId를 키값으로 하여 객체로 저장하면 O(1)만에 찾을 수 있겠군요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했습니닷!

Comment on lines 22 to 36
parseVideoEmbedUrl() {
return `https://www.youtube.com/embed/${this.videoId}`;
}

parseChannelUrl() {
return `https://www.youtube.com/channel/${this.channelId}`;
}

parseVideoUploadDate(date) {
const newDate = new Date(date);

return `${newDate.getFullYear()}년 ${newDate.getMonth()}월 ${newDate.getDate()}일`;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse 라는 동사를 잘못사용하고있는것같아요 👀
parse, parsing이 어떤 동작인지 좀더 자세히 공부해보면 좋을것같습니다! 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하하... 파싱이 아니고 create~로 수정해두었습니다! 감사합니다!

return `${newDate.getFullYear()}년 ${newDate.getMonth()}월 ${newDate.getDate()}일`;
}

toJson() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고유 명사들은 보통 모두 대문자로 써주는게 일반적입니다. (ex. ISO, HTML, JWT, etc)
toJSON

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗.. 감사합니다! 수정했습니다!

createTemplate() {
const fragment = document.createDocumentFragment();
const clip = createElement({ tag: 'article', classes: ['clip', 'd-none'] });
// clip.classList.add(...['clip', 'd-none']);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 커밋할때에는 항상 내가 커밋하는 코드를 다시 리뷰하는 습관을 들이시는게 좋습니다! :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉... 한번 더 체크하겠습니다!

textContent: this.channelTitle,
});
channelUrl.href = this.channelUrl;
channelUrl.target = '_blank';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

@zereight zereight Mar 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에서 관련 궁금증을 해결하였습니다!
target='_blank'로 새로 열린 사이트에는 window.opener가 존재하는데 이는 보안에 취약할 수 있으므로,
noopener를 사용하여 window.opener가 새로 열린 사이트에서 존재하지 않도록 해야하는군요!

noreferrer은 분석 및 추적 소프트웨어 수치를 왜곡시킬 수 있다는 단점을 보아서? 아직 적용은 시키지 않았습니다.
둘다 적용하는게 좋을까요?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noopener만 추가하시면 될것같아요!

async requestVideos() {
const url = this.createRequestUrl(this.searchTerm, this.pageToken);

const res = await fetch(url, { method: 'GET' })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch api의 default method는 뭘까요? 👀

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GET입니다. ㅎㅎ 생략해도되겠군요!

Comment on lines 9 to 18
export const updateVideosToBeShown = (items) => {
const videos = [];

items.forEach((item) => {
videos.push(new Video(item));
});

return {
type: UPDATE_VIDEOS_TO_BE_SHOWN,
payload: {
videos,
},
};
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const updateVideosToBeShown = (items) => {
const videos = [];
items.forEach((item) => {
videos.push(new Video(item));
});
return {
type: UPDATE_VIDEOS_TO_BE_SHOWN,
payload: {
videos,
},
};
};
export const updateVideosToBeShown = (items) => {
return {
type: UPDATE_VIDEOS_TO_BE_SHOWN,
payload: {
videos: items.map(item => new Video(item))
},
};
};

이정도는 사실 선호의 차이일 수도 있는데, 저는 개인적으로 추가로 변수선언하지않고 작성할 수 있다면 추가 선언을 지양하는 편이예요 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예전 리뷰어님한테서는 변수로 선언하라는 피드백을 받아서..! 🥶
map으로 videos에 받아서 넘겨주는 방식으로 코드 길이만 줄였습니다 ㅎㅎ

Comment on lines 22 to 24
getItem(key) {
return JSON.parse(localStorage.getItem(this.key[key]));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마찬가지로 JSON.parse에 파싱할수 없는 값이 들어오면 어떻게될까요? 어떻게 예외처리해야할까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러를 throw하기 때문에 try/catch으로 예외처리를 해주어서 undefined를 return하도록 수정했습니다!

Comment on lines 15 to 28
createRequestUrl() {
const pageTokenQuery = this.pageToken ? `pageToken=${this.pageToken}` : '';

return (
`https://www.googleapis.com/youtube/v3/search?` +
`part=snippet&key=${MY_KEY}
&q=${this.searchTerm}&maxResults=${MAX_RESULT}&type=video&${pageTokenQuery}`
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URLSearchParams라는 브라우저 API를 사용해도 좋을것같아요 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URLSearchParams 을 사용하여 URL를 만들어주었습니다!

cy.get('#youtube-search-input').type(searchTerm);
cy.get('#youtube-search-button').click();

cy.wait(10000);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cypress의 intercept라는 API에 대해 알아보세요 👍
https://docs.cypress.io/api/commands/intercept.html#Waiting-on-a-request

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cypress의 intercept는 cypress 코드에서 request를 직접보내어서 테스트 하는것 아닌가요?.?
사용자 행동에 기반하여 테스트하고 있어서.. 사용자가 검색버튼을 click했을 경우 충분한 시간을 가진 후에 결과를 체크하도록 만들었습니다!
intercept를 사용하려면 youtubeAPI를 request하는 함수의 기능만 테스트할 수 있을것 같은데요 ㅜㅜ.
제가 잘못 알고 있는건가요...!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 유튜브서버의 문제로 응답이 10초안에 오지 않으면 어떻게될까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 저 cypress코드서는 10초 이상이 걸릴 경우에 테스트가 통과되지 않습니다 ㅜㅜ
비동기 테스트할 방법을 찾다가.. 울며겨자먹기로 cy.wait를 사용했는데요!
이에 관해서 계속 알아보고 있습니다. 혹시 힌트가 있을까요? 🙄

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 드렸던 링크가 힌트입니다.

Copy link
Author

@zereight zereight Mar 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗.. 제가 docs를 잘못읽었네요!
요청을 감지하여 사용할 수 있는 것이었군요..! 수정하니까 완전! 테스트하기 용이해졌습니다. 감사합니다! >_<

@zereight
Copy link
Author

zereight commented Mar 6, 2021

리뷰어님 코멘트 감사합니다!
말씀 주신 not-allowed속성 적용했고 innerHTML에 관한 이슈들을 다시 짚어보았습니다. 감사합니다!

그런데 render함수에서 상태가 변경되었는지 조건문으로 확인하는게 아니라, 상태가 변경되었을 때만 render함수를 부르도록 만들면
이 말씀은, store의 dispatch()에서 특정 state를 검사해서 바뀌었으면 구독중인 subscriber중 몇몇만을 호출되도록 구현하라는 의미일까요? 어떻게 개선해야할지 계속 고민중에 있습니다!

페어와 리팩토링 하는 동안 코드수정이 몇번 있었습니다!

  • 모달 외부 클릭 시, video-search-overlay라는 modal-inner의 형제 element를 두어서 modal이 close되게 구현
  • iframe에 srcdoc 속성을 두어 좀 더 빠르게 동영상이 로딩되는 것처럼 보이게 하기

Comment on lines 15 to 26
createRequestURL() {
const requestURL = `https://www.googleapis.com/youtube/v3/search?`;
const searchParams = new URLSearchParams("part=snippet&type=video");

searchParams.set('key', MY_KEY);
searchParams.set('maxResults', MAX_RESULT);

if(this.pageToken) {
searchParams.set('pageToken', this.pageToken);
}

return `${requestURL}${searchParams}`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const params = new URLSearchParams({
  a: 1,
  b: 2,
})

params.toString()

이렇게 작성할수도있습니다

Copy link
Author

@zereight zereight Mar 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 꿀팁 감사합니다! 👍👍👍👍👍
코드에 적용해보았습니다!

Copy link

@ysm-dev ysm-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다! 👏 다음단계에서 봬요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants