1.소개

간단한 영화 소개 사이트입니다. 디자인과 기능은 네이버, 다음, 넷플릭스, 왓챠, 웨이브를 참고하였습니다. 영화 정보 는 TMDB에서 제공하는 open api를 사용하였습니다.

사이트: https://movieinfoservice.netlify.app/

2.기술스택

  • typescript: 동적 언어인 js 를 보완하여 런타임 이전에 오류를 발견할수 있도록 하기위해서 사용하였습니다.
  • react: spa 웹을 만들기위해서 사용하였습니다.
  • redux: 전역 상태 관리를 위해서 사용하였습니다.
  • redux-toolkit: 리덕스를 조금더 간단하게 사용하기 위해 도입하였습니다
  • redux-saga: api데이터를 받아오기 위해서 사용하였습니다.
  • styled-component: js 파일내에 css 를 작성하기위해서 사용하였습니다.
  • axios: api 데이터를 받아오기 위해서 사용하였습니다.
  • jest: 테스트를 위해 사용하였습니다.
  • testing-library: 테스트를 위해 사용하였습니다.

3.페이지별 소개

7개 종류의 페이지가 존재합니다.

  • 상영중인 영화중에서 첫번째 영화의 예고편이 제일위에 재생됩니다.
  • 상영중인영화, 개봉예정영화가 아래에 로드됩니다.
  • 포스터에 마우스를 올리면 영화 줄거리를, 클릭하게되면 영화의 상세정보를 볼수 있습니다.

랭킹

  • 인기순,별점순으로 정렬할수 있는 버튼과, 버튼에 따라 포스터가 로드됩니다.
  • 포스터에 마우스를 올리면 영화 줄거리를, 클릭하게되면 영화의 상세정보를 볼수 있습니다.

영화 검색

  • 인풋박스를 클릭하면 최근검색어가 나옵니다.
  • 영화제목을 검색하고, 검색아이콘을 클릭하면 결과가 아래에 렌더링됩니다.
  • 결과 포스터를 클릭하면 영화의 상세정보를 볼수 있습니다.
  • api에서 제공하는 검색결과가 2페이지 이상일경우 아래에 페이지네이션이 구현되어 있어 다른 숫자를 클릭하면 정보를 다시 받아옵니다.

나만의영화

  • 영화 상세정보에서 옆의 별표를 클릭해서 저장한 즐겨찾기 영화가 표시됩니다.
  • 유저 정보에 저장하기 때문에 유저별로 저장한 영화가 표시됩니다.
  • 포스터를 클릭하면 상세 정보로 이동할수 있습니다.
  • 영화 갯수가 20개가 넘어가면 한번에 렌더링 하지 않고, 먼저 20개를 렌더링한후 스크롤을 내리면 추가적으로 10개씩 영화가 렌더링 됩니다.
  • 드래그앤 드롭을 사용하여 즐겨찾기한 영화를 삭제할수 있습니다.

영화상세정보

  • 영화 포스터, 상세정보, 요약이 윗쪽에 렌더링됩니다.
  • 별모양을 클릭하면 즐겨찾기를 할수 있습니다. 이때 별이 채워지면 즐겨찾기가되고, 비워지면 해제됩니다. 즐겨찾기 버튼은 로그인 해야만 활성화됩니다.
  • 아래에는 댓글을 달수 있습니다. 댓글을 달면 영화, 유저별로 서버에 저장되며, 본인의 댓글의경우 삭제할수 있습니다.

로그인

  • 소셜로그인 두종류와 로컬 로그인 두종류를 지원합니다.
  • 네이버 로그인의경우 현재 검수를 받지 않아 관리자 계정만 이용가능하며, 카카오의 경우 이용가능합니다.
  • 로컬 로그인의 경우 저장한 데이터를 현재 변경할수는 없게 되어있습니다.

회원가입

  • 소셜로그인의경우 회원가입이 필요없어 로컬로그인의 경우에만 회원가입을 진행합니다.
  • 입력사항을 입력한 후에는 변경할수 없습니다.

4. 프로젝트 진행시 경험한 내용

1. 무한 스크롤

리액트 요소의 ref속성에 react-intersection-observer 라이브러리가 제공하는 훅의 값을 담아놓으면 이 요소가 화면에 보일때는 true로, 보이지 않을때에는 false로 바뀌기 때문에 10번째 항목마다 ref에 값을 담아두고, view값이 변화할때 마다 요소를 10개씩 추가하는 방식을 사용하여 나만의 영화 페이지에 무한스크롤을 구현하였습니다.

2. 이미지 preloading

처음에 home의 포스터 리스트를 제작하였을때, 다음 포스터를 얻기위해 화살표를 누르면 다음 포스터들이 바로 로딩되지 않고, 일정시간후에 로딩되는것이 사용자 경험에 좋지 않다고 생각하여서 처음 5개의 포스터를 렌더링할때 미리 나머지 포스터도 로드하여 쿠키에 저장해 놓는 함수를 만들어 사용하였습니다.

3. redux, redux-toolkit

처음 프로젝트를 도입할때에는 리덕스를 사용하였는데, 보일러플레이트 코드가 상당히 많고 복잡하여 이후 유지보수를 하는데 문제가 있었습니다. 이를 해결하기 위해 먼저 saga 함수와 reducer부분을 두파일로 분리하고, 하나의 폴더에 담았습니다. 또한 보일러 플레이트 코드를 줄여주는 redux-toolkit 을 사용하여 불필요한 코드를 상당히 줄였습니다.

또한 이번 개선으로 상태값을 일괄적으로 정리하였습니다. 기존에는 error, loading 처리가 다소 미흡하였는데, 특정 예외를 제외하고는 각각의 상태값에 error, loading 프로퍼티를 만들고 이를 boolean으로 관리하여 해당 상태 유무를 처리하고, 해당 상태의 값은 content 프로퍼티에 담아서 일관성을 지키려고 노력하였습니다.

4.드래그 앤 드롭 api를 사용하여 즐겨찾기 삭제하기

즐겨찾기 페이지에는 삭제 기능이 없어서, 이를 드래그앤 드롭 api를 사용하여 삭제기능을 구현하였습니다. 리액트에서 사용할수 있는 라이브러리가 있었지만, 간단한 기능이므로 사용하지 않고 브라우저에서 제공하는 api를 이용하여 만들었습니다. 기능은 포스터를 드래그하면 아래에서 상자가 나와서 이 상자안에 넣으면 삭제되는 기능입니다. 이때 드래그한 포스터의 정보를 얻기 위해서 dataTransfer객체에 데이터의 인덱스 정보를 저장하여 이후 삭제할때 이 데이터를 참고하였습니다.

5.로딩, 에러 ,예외처리

처음 프로젝트를 진행할때, 도메인별로 로딩, 에러, 예외처리에 일관성이 없어서, 리덕스를 개선하면서 에러, 로딩 프로퍼티를 추가하고 이를 이용해서 각 도메인의 프레젠테이션 컴포넌트에 로딩, 에러,컨텐츠가 없는등의 예외상황도 처리하였습니다.

6. 타입스크립트

이 프로젝트를 시작하면서 처음 타입스크립트를 작성하여서 처음에는 any 타입을 다수 표기하였습니다. 이후 계속 리팩토링을 해나가면서 any 타입을 지우고, 적절한 타입을 넣는것이 타입스크립트를 쓰는 이유가 되는것이라 생각하였고 현재는 any 타입이 거의 없는 상태 입니다. 이러한 타입스크립트는 객체를 사용할때 프로퍼티를 추론해주거나, 혹은 해당 타입이 사용가능한 내장함수를 추론해준다는 점이 런타임 이전에 발생할 오류를 줄이는데 큰 도움을 준다고 생각합니다.

7. 백엔드 작업

이번 프로젝트에서는 다른 백엔드와 작업하지 않고, 개인적으로 백엔드를 공부하여 유저, 댓글 ,좋아요 기능을 구현하였습니다. 작업하다 보니 프론트에서 넘어오는 변수의 대문자 소문자 여부에 따라서 백엔드의 오류를 발생시키는 경우도 있었고, post, get 방식 혹은 api 주소 가 일치하지 않아도 오류가 발생하였으며, 넘기는 데이터가 비어있지 않아야하는데 비어있는 경우에도 오류가 발생하였습니다.

물론 대소문자나 빈값여부 때문에 발생하는 오류는 해결하기 어렵지 않지만, 처음부터 백엔드와 잘 소통한다면 발생하기 어려운 문제이기도 하고, 이러한 문제도 쌓이게되면 불필요하게 시간이 소비되기 때문에 이후 백엔드와 같이 협업을 하게 된다면 기본적인 부분에서 컨벤션 을 합의하거나, 중간에 변경하더라도 소통을 잘해야함을 느꼈습니다.

상세한 내용은 백엔드 repository의 readme 에 기록하였습니다.

8.CORS & api 키 보안

처음 로컬에서 api데이터를 받아올때 발생한 cors 오류는 package.json에 proxy속성에 주소를 추가하여 proxy서버를 설정하는 간단한 방법으로 해결하였습니다.

하지만, 이는 개발시에만 적용할수 있는 방법으로, 결국 배포를 위해 빌드할때 문제가 발생하였습니다. 구글링을 통해서 내린 결론은 결국 서버가 필요하다는 판단이었습니다. 서버로 호출하게 되면, 서버와 서버 사이에는 cors 가 발생하지 않으므로 문제가 해결되고, 덤으로 api키가 프론트측에서 담아 보내면 노출되는데, 서버에서 호출하게되면 프론트단에 노출되지 않아 보안의 효과까지 거둘수 있어, 간단한 서버를 만들기로 하였습니다.

서버를 만들어본경험이 없어서 최대한 동작만 하는 서버를 비슷하게 만들려고 하다가, netlify에서 간단하게 serverless function 을 제공하여 이를 이용하여 더 간단하게 만들어보고자 하였습니다. 구글에서 찾아본 예제는 전부 get으로 요청하는 방식 뿐이었지만 get과 post 방식이 크게 다르지 않을것이라고 생각하여 그대로 작성하였으나, 계속 cors오류가 발생했습니다. 추가적으로 찾아본 결과 post의 body에 데이터를 담아서 전송하는 경우에는 simple request 가 아닌 preflight를 사용하여 예비전송을 한번 시행함을 알게 되었습니다. 따라서 서버에서 이를 처리해주는 로직을 추가하여 cors 문제를 해결할수 있었습니다.

9.useEffect 의존성 배열

영화 상세페이지에서 즐겨찾기기능을 구현하는 도중, 빈별을 클릭하면 별이 채워지면서 로컬스토리지에 저장은되는데, 다시 영화 상세페이지로 들어와보면 즐겨찾기 별이 채워져있지 않았습니다. 로컬스토리지에는 저장되어 있는데 표시가 안되는 원인을 찾아보니, 실제 데이터는 리렌더링시 받아오게 되는데, useEffect의 의존성 배열을 비워 마운트시에만 실행되었기에, 기본값을 참고하여 항상 비어있었던것 입니다. 간단하게 의존성배열에 데이터를 담은 변수를 넣어 문제를 해결하였지만, useEffect를 사용할때, 의존성 배열의 사용을 최대한 줄이고, 사용시 로직에 대해서 충분히 이해한뒤 사용해야 함을 느꼈습니다.

10. 코드 폴더 구성

처음 프로젝트를 진행할때는, 프레젠테이션 컴포넌트끼리, 컨테이너 컴포넌트끼리 파일을 구성하였습니다. 구성요소가 공통점을 가지고 있어서 같은 폴더에 담는데에 논리적으로 오류가 있지는 않았지만, 이후 프로젝트를 수정할때, 보통 같은 프레젠테이션과 컴포넌트 파일을 수정하는 경우가 많았기에, 이후 컴포넌트 폴더구조를 도메인 별로 나누고, 아래에 프레젠테이션, 컨테이너, 스타일 파일을 넣는 방식으로 수정하였습니다.

또한 폴더 내에는 container 컴포넌트와 hook 컴포넌트가 존재합니다. 이번 프로젝트에서는 중복해서 사용할만함 hook 컴포넌트가 없어서, 두 컴포넌트를 나누는데에 의미가 없을수 있지만, 기본적으로 container 컴포넌트는 커스텀 훅을 여러가 받아서 사용하거나 혹은 리덕스의 데이터만을 받는 아주 간단한 로직만을 처리하는 컴포넌트이며, 대부분의 로직은 hook 에서 처리됩니다. hook의 경우 공통 로직이 없다면, 기본적으로 도메인내에서 공통으로 사용되도록 한 hook에 로직을 담았습니다. 컨테이너 컴포넌트에 로직을 담을수도 있지만, hook 테스팅 툴을 이용하기 위해 커스텀 훅을 구성하는 편이 편리 하였기에 이러한 구조를 선택하였습니다.

11. 유저 인증기능

node js 를 학습하면서, 이에 대한 결과물로 기존 프로젝트에 로그인 기능을 추가하였습니다. 원래 프로젝트는 유저를 인식하지 못해서, 정보를 로컬스토리지에 저장하는 방식이었는데, 로그인 기능을 추가하면서 유저별로 댓글, 좋아하는 영화를 관리할수 있게 되었습니다. 이에따라 관리해아할 상태가 크게 증가하였고, 서버에 요청해야할 데이터가 증가하였습니다. 이에 제가 리덕스를 정리한 방식은 최대한 기능별로 정리를 하고, promise를 한번에 호출하도록 로직을 구성하였습니다. 따라서 현재 페이지 에서는 로그인 유무를 체크하는 로직을 제외하면 모든 페이지에서 promise를 한번에 처리하도록 하고있습니다.

한편 user 리덕스의 경우 회원가입, 로그인 체크, 로그아웃을 관리하는 액션을 구성해두었는데, 처음에는 회원가입 로직을 해당 리덕스에 포함하는것을 고려하였습니다. 그러나 소셜 로그인의 경우 회원가입을 필요로 하지 않고, 회원가입의 에러처리를 직관적으로 해보고자 리덕스 외부에 회원가입로직을 위치시켰습니다.

12. 보안

패스워드의 경우 초기에 값을 그대로 넘겨주어서 브라우저상에 패스워드가 그대로 노출되는 문제가 있었습니다. 따라서 백엔드에서 단방향 암호화는 유지하되, 프론트에서 양방향 알고리즘을 적용하여 패스워드를 암호화한뒤 이를 백엔드에서 풀어내서 다시 단방향 암호화하는 방식을 적용하였습니다.

13. 테스트

이전까지는 브라우저 위에서 의도한 대로 작동하는지 실제 테스트를 진행하였으나, 기본적은 단위 테스트가 필요하다고 생각이 되어서 도입하게 되었습니다. 기본적으로 jest, testing-library를 이용하였고, 리덕스 테스트를 위해서는 redux-mock-store를 통해 가짜 스토어를 이용하였고, 리덕스 사가의 경우 redux-saga-test-plan를 이용하여 테스트 하였습니다. 또한 hook 테스트를 이용하기 위해서 @testing-library/react-hooks를 이용하였습니다

먼저 프레젠테이션 컴포넌트의 경우 스냅샷 테스트, 의도한 이벤트 발생 여부, 의도한 텍스트 존재 여부를 테스트 하였습니다. 컨테이너 컴포넌트의 경우 hook을 테스트 하기 위해 hook 파일로 분리하여서 함수의 동작을 테스트 하였습니다. 리덕스의 경우 액션 생성함수, 리듀서, 사가 함수의 동작을 테스트 하였습니다.