React.js Codelab 2016 – Express 와 React.js 를 사용한 웹 어플리케이션 만들기 (4)


3편에서는 계정인증 부분을 완료했었습니다.

어떤가요? 조금 복잡했나요? 이 파일 수정하고.. 저 파일 수정하고, 조금 정신없었을 수도 있지만,

결국엔 비슷한 동작이 계속해서 반복되기 때문에 몇번 해보면 쉬워진답니다.

이런 동작이 반복되죠

  1. action type 만들기
  2. action creator 만들기
  3. reducer 만들기
  4. 컴포넌트에서 사용

이번 편에서는 메모를 작성하는 부분을 완성해보도록 하겠습니다.


16. Write 컴포넌트 만들기

wrte

Codelab 링크: http://codepen.io/velopert/pen/LkZKpx

 

Write 컴포넌트 생성 (src/components/Write.js)

 

컴포넌트 인덱스에 Write 컴포넌트 추가 (src/components/index.js)

 

Home 컴포넌트에서 현재 로그인상태라면 Write 보여주기

 

Write 컴포넌트를 위한 스타일 추가 (src/style.css)

 

Write 컴포넌트 뷰 만들기 (src/styles.css)

dd

위에 Navbar 랑 Write 컴포넌트랑 너무 가깝지 않나요?

Home 에서 여백을 주도록 하겠습니다 (Write 에서 하지 않는 이유는, 나중에 메모를 읽을떄도 여백이 필요하기 때문인데, 로그인상태가 아니라면 Write가 보여지지 않기 때문입니다)

 

style.css 에 wrapper 클래스 추가 (src/style.css)

 

Home 컨테이너 컴포넌트에 wrapper 스타일 클래스 적용 (src/container/Home.js)

 

Write 컴포넌트 textarea 에 state 사용, defaultProps / propTypes 설정 (src/components/Write.js)

 

Action Type 추가하기 (src/actions/ActionTypes.js)

 

memo 액션파일 만들기 (src/actions/memo.js)

 

memoPostRequest 구현하기 (src/acitons/memo.js)

 

memo 리듀서 만들기 (src/reducers/memo.js)

 

리듀서 인덱스 수정 (src/reducers/index.js)

 

Home 컨테이너 컴포넌트에서 Redux 연결 (src/container/Home.js)

postStatus 와 memoPostRequest 가 매핑되었습니다.

 

Home 컨테이너 컴포넌트에 handlePost 구현하기 (src/container/Home.js)

아직 구현은 하지 않았지만, 메모가 작성되고 나면, 새 메모를 불러오도록 할 것입니다.

로그인 상태가 아니라면 알림을 띄우고 2초뒤 새로고침합니다.

handlePost 를 onPost 로 Write 컴포넌트에 전해줍니다.

 

Write 컴포넌트에서 위에서 받은 onPost 사용하기 (src/components/Write.js)

 

여기에도, handlePost 메소드를 만들어주세요. .then() 을 통하여 작성이완료되면 내용을 비웁니다.

POST 버튼이 클릭 됐을 시 handlePost 를 실행하게 하세요.

작성기능을 얼추 끝났습니다. 이제 읽기 기능을 구현 하고나면 다시 작성기능을 수정하겠습니다 – 작성 후 새 메모 불러오기

 

17. 읽기 기능 구현하기 – Memo 와 MemoList 컴포넌트

dfg

Codepen 링크: http://codepen.io/velopert/pen/YWYoQY?editors=1111

메모 컴포넌트는 위와 같이 자신의 메모라면 수정/삭제 를 할 수 있도록 옵션 버튼이 우측상단에 생깁니다.

Materializecss 의 Dropdown 기능을 통하여 저 옵션메뉴를 구현하였습니다.

Edit 버튼을 누르면 컴포넌트가 Write 컴포넌트와 비슷하게 변하도록 할 것입니다 (이 부분은 나중에 구현합니다)

 

Memo 컴포넌트 파일 생성 (src/components/Memo.js)

 

MemoList 컴포넌트 파일 생성

 

컴포넌트 인덱스에 추가 (src/components/index.js)

 

Memo 컴포넌트를 위한 스타일 추가 (src/style.css)

 

Memo 컴포넌트 뷰 만들기 (src/components/Memo.js)

 

MemoList 에 Memo 컴포넌트 렌더링하기

지금으로서는 임시로 Memo 컴포넌트 하나만 렌더링합니다

 

Home 컴포넌트에 MemoList 컴포넌트 렌더링하기 (src/components/Home.js)

 

Memo 컴포넌트 propTypes, defaultProps 지정하기 (src/components/Memo.js)

이 컴포넌트는 Memo에 필요한 값들을 객체형으로 받아오게 하겠습니다.

나중에 컴포넌트 Mapping 을 할텐데 이렇게 하는편이 편하기 때문입니다 (원한다면 객체형으로 하지 않고 분리하셔도 됩니다)

ownership prop  은 해당 메모가 자신의 메모인지 아닌지 여부를 확인하는 값입니다.

 

Memo 컴포넌트 렌더링 할 때 props 값 사용하기 (src/components/Memo.js)

4번줄의 코드는 ES6 의 비구조화 할당입니다

“1 second ago” 형식으로 편하게 계산하여 나타내기 위하여 React-Timeago 라는 컴포넌트가 사용되었습니다.

아직 Edited 시간을 알려주는 부분과 현재 Star 를 자신이 클릭했는지 안했는지 나타내는부분은 구현되지 않았습니다.

...${expression}... 같은 표현은, ES6의 Template Literals 라는 문법입니다. 문자열 템플릿 안에 변수/상수 값을 손쉽게 넣을 수 있습니다.

(이걸 사용하지 않는다면 ‘dropdown-‘ + ___  뭐 이런식으로 했어야겠죠? Template Literals 를 사용하면 읽기가 더 편합니다.

 

Memo 컴포넌트 memo View 를 따로 분리하기 (src/components/Memo.js)

위와 같이 card div 부분을 잘라내서 memoView 상수에 담고, 렌더링할때 memoView 를 렌더링하도록 설정하세요.

이렇게 한 이유는, 나중에 Edit 모드일때는 Write 와 비슷한 뷰를 보여주게 할 것이기 때문에 미리 작업을 한것입니다.

 

Memo 컴포넌트의 Dropdown 메뉴 추가 작업

Dropdown 메뉴가, 자신의 메모일때만 보여주게하고 (ownership 이 true일때)

Dropdown 메뉴 활성화 작업을 componentDidMount 와 componentDidUpdate 에서 하도록 하겠습니다.

지금으로서는, 활성화를 따로 안해도 작동하긴 하지만, 해당 컴포넌트가 유동적으로 생성되고 업데이트될 경우에는 저희가 따로 활성화작업을 해야합니다.

 

MemoList 컴포넌트 propTypes 및 defaultProps 설정 (src/components/MemoList.js)

 

Home 컨테이너 컴포넌트에서 Mock Data 전달 (src/components/Home.js)

원래는 AJAX 요청을 하여 서버에서 데이터를 불러와야하지만

그 작업을 하기 이전에 테스팅을 위하여 mock data (테스트용 가짜 데이터) 를 만들어서 전달하세요

 

MemoList 컴포넌트 받은 데이터 배열을 컴포넌트 매핑

데이터배열을 컴포넌트배열로 매핑한후 렌더링하였습니다.

Mock 데이터를 잘 렌더링했나요? 그렇다면, 이제 서버에서 데이터를 가져올 차례입니다

 

Action Type 추가하기 (src/actions/ActionTypes.js)

 

memo 액션파일 수정하기 (src/actions/memo.js)

memoListRequest 의 경우 지금까지 만들었었던 thunk 들과는 달리 파라미터들이 좀 많은데요,

나중에 추가적으로 메모를 로딩할때 ( 새 메모 로딩 및 오래된 메모 로딩), 그리고 특정유저의 메모를 로딩할때도 이 함수가 사용됩니다.

 

memoListRequest 구현하기 (src/actions/memo.js)

지금은 일단, 초기 로딩 부분만 구현하였습니다.

 

memo 리듀서 수정하기 (src/reducers/memo.js)

isLast 값은 현재 로딩된 페이지가 마지막페이지인지 아닌지 알려줍니다.

한 페이지에 6개의 메모를 보여주는데요, 로드한 메모가 6개 미만이라면, 더 이상 메모가 없다는것을 의미합니다.

(저희가 나중에 무한 스크롤링 기능을 구현할텐데 마지막 페이지에 도달하면 스크롤링이 멈추도록 해야겠죠?)

 

Home 컨테이너 컴포넌트에서 memoListRequest 사용하기 (src/containers/Home.js)

mapStateToProps 에 state.authentication.status.currentUser 과 state.memo.list.data 를 연결해주고, mapDispatchToProps 에 memoListRequest 를 전달해준다음,

componentDidMount()에서 memoListRequest 함수를 사용하세요. 실행 후, 콘솔에 결과를 프린트하도록 해보세요.

잘 됐나요?

 

Home 컨테이너 컴포넌트 서버에서 받은 결과값 MemoList 로 전달 (src/containers/Home.js)

아까전에 만들었었던 mockData 를 지우고, 진짜 데이터를 MemoList 로 전달해주세요.

currentUser props 도 실제 currentUser 값을 전달하세요.

식은죽 먹기죠? 브라우저에서 잘 로딩하는지 테스트해봅시다.

CHECKPOINT: https://github.com/velopert/react-codelab-memopad/tree/17_MEMO_READ

 

18. 읽기 기능 구현하기 – 추가 로딩 (새 메모 / 이전 메모)

새로운 메모 혹은 이전 메모를 읽어오는것 생각보다 간단합니다.

현재 페이지에 로딩되어있는 데이터 중에서 (초기로딩 된 데이터) 가장 위에 있는 메모의 _id 값보다 높은 _id 를 갖고있는 메모를 쿼리하면 새로운 메모들을 읽을 수 있고

가장 아래에 있는 메모의 _id 값보다 낮은 _id 를 갖고있는 메모를 쿼리하면 이전 메모들을 읽을 수 있습니다

그럼, Express 라우터에 API를 추가해봅시다

메모 추가 로딩 API 구현하기 (server/routes/memo.js)

 

memoListRequest 추가구현하기 (src/actions/memo.js)

저희가 방금 구현한 API 에 맞춰서 파라미터에 따라 URL 을 설정해줍니다.

 

memo 리듀서 MEMO_LIST_SUCCESS 부분 수정하기 (src/reducers/memo.js)

배열의 앞 부분에 데이터를 추가할땐 $unshift 연산자를, 뒷 부분에 추가 할 땐 $push 연산자를 사용합니다.

 

Home 컨테이너 컴포넌트에서 5초마다 새 메모 로딩 (src/containers/Home.js)

mapStateToProps 에서 state.memo.list.status 를 매핑해주세요.
loadNewMemo 는 새 메모를 읽어들일 때 사용되는 메소드입니다.

메모 요청 상태가 ‘WAITING’ 일 때는 로딩을 하지 않도록 하게 했는데요,
잠시 후, 새 메모를 작성 할 때 새 메모를 읽게 끔 트리거 하는 기능도 구현 할 텐데, 상태가 ‘WAITING’ 일때 무시하는 코드를 넣지 않으면
똑같은 요청을 두번 할 수 도 있게 되기 때문입니다.

이 부분에서 그냥 return 을 해도 되지만, 비어있는 Promise 를 리턴한 이유는, Write 에서 해당 메소드를 입력하고 .then 을 사용 할 수 있게 만들기 위함입니다
(메소드를 실행 하고, 성공메시지 / Write 내용초기화를 할건데, 여기서 그냥 return; 을 날려버리면 만약에 요청이 중첩됐을 때 먹통이 됩니다)

그리고, 페이지가 비어있을 경우에는 초기로딩을 시도하도록 하였습니다.

constructor 에서 this 와 binding 해주세요.

componentDidMount 에서, timeout 기능을 통하여 이 작업을 5초마다 반복하도록 설정하였습니다.

 

작성 시, 새 메모를 읽도록 트리거하기 (src/containers/Home.js)

loadMemo 라는 props 를 받아와서 메모작성이 성공하였을때 실행하도록 하였습니다
메모가 작성되면, 새 메모를 읽어오도록 명령하고, 해당 작업이 끝나면 성공했다고 알림을 띄웁니다.

 

간단하죠?

한번 메모를 작성해봅시다. 바로바로 리로딩이 되나요?

 

19. 무한 스크롤링 구현하기

f

코드펜 링크: https://codepen.io/velopert/pen/grRAbQ

무한 스크롤링을 구현하기 전에, 위 링크를 들어가서 원리를 파악하도록 해봅시다.

 

무한스크롤링의 원리는 생각보다 간단합니다.

스크롤바의 위치를 계산해서, 스크롤바가 아랫부분에 닿으면 (혹은 가까워지면) 내용울 추가하도록 하는 것 입니다.

위 예제에선 text 를 jQuery 를 통하여 append 하도록 하였지만,

우리는, 굳이 append 를 쓸 필요 없이, memoListRequest 로 이전 메모들을 불러오도록 명령하면 되겠죠?

스크롤바의 위치를  계산하는 부분에서, 우린 꼭 jQuery를 사용 할 필요는 없습니다.

순수 자바스크립트를 사용 할 수도 있지만, jQuery를 사용하는편이 더 쉽고, 어짜피 Materializecss 때문에 jQuery를 로드한 상태니
jQuery를 사용하도록 하겠습니다.

 

Home 컨테이너 컴포넌트 스크롤 리스너 작성 (src/containers/Home.js)

이렇게 코드를 추가하고나서 테스팅을 해보세요.

(스크롤바가 없어서 테스팅을 못한다면 페이지를 확대하거나 창의 크기를 줄여보세요)

스크롤을 해보면, 스크롤바가 하단에 가까워졌을시, 콘솔에 LOAD NOW 가 출력되는데,

v

그 구간에서 스크롤 될 때마다 출력이 된다는거죠..

저희는 그 구간에 들어갔을때 처음에만 한번 코드를 실행하게 해야하는데, 이 문제는 state 를 사용하여 간단하게 해결 할 수 있습니다.

한번 코드를 테스팅해보세요. 잘 되나요?

 

이제 실제 기능을 구현해보겠습니다

이번에 수정된 코드가 꽤 많은데요, 우선 mapStateToProps 부분에 state.memo.list.isLast 를 연결해주었습니다.
이는 마지막 페이지에 도달 했을 시, 요청을 취소하기 위함입니다.

그리고 loadOldMemo 라는 메소드를 만들었습니다. 만약에 현재 읽고 있는 페이지가 마지막 페이지라면 요청이 취소됩니다.
나중에 이 메소드를 사용하고 .then() 을 사용 할 수 있도록 취소 할 땐, 비어있는 Promise 를 리턴합니다.

이전 메모들을 불러오기위하여 페이지에 로드된 메모 중 최하단 메모의 id 를 API로 전해줍니다.
API 를 실행 후, 만약에 방금 읽어들인 페이지가 마지막페이지라면 알림을 띄웁니다.

loadOldMemo 메소드가 완성되었다면, constructor 에서 this binding 을 해주고,
componentDidMount 에서 아까 console.log 를 loadOldMemo(); 로 바꿔주세요.

마지막으로, componentWillUnmount 에는 스크롤 리스너를 제거하는 unbind 코드를 추가합니다.

자, 이제 테스트를 해보세요. 잘 되나요?

 

버그잡기.. (src/containers/Home.js)

무한스크롤링 구현이 다 끝난것 같지만! 아직 안 끝났습니다.

지금 문제점이 하나 남아있는데요, 다음 이미지를 살펴봅시다.

zxcc

만약에 사용자 화면의 해상도가 무척 높다면..  스크롤바가 생기지 않겠죠?
이럴 경우엔 메모가 더 있음에도 불구하고 화면을 확대하거나 창 크기를 줄이지 않는이상 이전 메모들을 읽을 방법이 없습니다.

한번 여러분들도 페이지를 축소한다음에 새로고침을 해보세요.
이렇게 문제가 있는걸 아는데.. 그냥 넘어갈 수는 없겠죠?

해결 방법은, 초기 로딩을 한다음에, 스크롤바가 만약에 안생겼다면 이전 메모로딩을 스크롤바가 생길 때 까지 반복하면됩니다.
스크롤바가 있는지 없는지 체크를 하려면 다음 값을 비교하면됩니다: $(body).height() < $(window).height() 

재귀적인(Recursive) 한 방법으로 버그를 해결하였습니다.

자, 이제 남은 작업들은 메모 수정/삭제/별주기/유저검색 입니다.

지금까지 잘 따라와주셨다면, 앞으로 할 것들도 쉽게 끝낼 수 있을거에요.

  • 김범준

    좋은 강좌 감사드립니다.
    덕분에 보고 많이 배우고 있습니다.

    이번 강좌에서 containers/Home.js 파일의 경우
    authentication 이 authentification 으로 잘못 기재된 것 같습니다.

    확인 부탁드립니다.
    감사합니다.

    • 강좌에서 fi 를 붙인곳이 많네요
      ㅋㅋ이때 헷갈렸었나봐요..

      github저장소에 올린건 제대로 했었는데..

      조만간 전체적으로 수정하도록 하겠습니다.

  • 조재민

    질문있습니다.
    React에서 Materialize css를 사용할 때 public/index.html에 몇 가지 코드를 추가해서 사용하셨는데, 이런 식으로 하면 모든 Materialize의 component들을 다 사용할 수 있는건가요? 제가 따로 SideNav를 추가하려고 같은 코드를 찾아서 복붙해서 실행시켜보니 demo에서 보여지는 것과 너무 차이가나네요. 혹시 React에서 materialize를 사용할 때는 추가로 해줘야하는 작업이 있는건가요? 제가 복붙한 코드의 링크는 http://materializecss.com/side-nav.html 입니다.

    • 우선 붙여넣으시고 class를 className으로 교체하시는거 잊지마시구요, componentDidMount에서 initialize해주시면 문제없이 작동할거예요

  • hspark

    immutable을 사용하지 않고 스프레드 연산자로 배열에 어떻게 추가하나요???
    memo reducer에서 새 메모 받아 오는데 스프레드 연산자로 하려고 했는데 안되네요.ㅠㅠ

  • ggoban

    강의와 진행을 다르게 해서 그런지 문제가 발생하면 처리하는데 한참 걸리네요 ㅠ session 문제가 지속적으로 발생하는데 결국 원인은 express sessin id 가 매번 새로 생성이 되서(로그인 시 생성된 세션, 메인으로 이동하면 새로운 세션, f5를 누르면 또 새로운 세션) loginInfo 를 정상적으로 찾지 못해 발생합니다. 관련된 내용을 검색해보곤 있는데 마땅한 해결책이 없네요.. 편법으로는 해결이 가능하긴 한데..OTL