13-5. 덧글 창 토글 하기
이제 기본적으로 덧글 창을 숨기고, 덧글 아이콘이 클릭될 때 마다 열고 닫는 기능을 구현해보겠습니다.
CommentBlockContainer 에 상태 연결하기
이번에 CommentBlockContainer 를 연결할때는, 지금까지 리덕스의 상태를 연결 할 때 해왔던것과 조금 다릅니다. connect 의 첫번째 파라미터로 들어가는 함수에서 (state)
가 아닌 (state, ownProps)
이렇게 두개의 값이 들어가게 하였는데요, 여기서 ownProps 는 컴포넌트가 렌더링 될 때 부모 컴포넌트에게서 받은 props 값을 가르킵니다.
우리는 나중에 이 컴포넌트를 렌더링 하게 될 때, post 객체를 props 로 전달받게됩니다. 그리고, 상태연결이 되는 과정에서, 부모 컴포넌트에게서 받은 post 의 _id 값을 기준으로, comments Map의 데이터를 참조합니다.
그 다음에는, 렌더 함수에서 status 를 toJS() 를 통해 객체로 변환하고 visible 값과 value 값에 대한 레퍼런스를 만드세요. 덧글 창이 처음 열리기 전엔, status 가 존재하지 않기 때문에 status 가 undefined 이기 때문에, 만약에 status 가 없을때는 객체 비구조화 과정에서 비어있는 객체 { }
를 사용하도록 합니다.
visible 값이 true 가 아닐때는, null 을 반환함으로서 아무것도 보여주지 않도록 설정하세요.
src/containers/Shared/PostList/CommentBlockContainer.js
import React, { Component } from 'react';
import CommentBlock from 'components/Shared/PostList/CommentBlock';
import { connect } from 'react-redux';
class CommentBlockContainer extends Component {
render() {
const { status } = this.props;
const { visible, value } = status ? status.toJS() : { }; // status 가 존재하지 않는 경우를 위한 예외 케이스
if(!visible) return null; // visible 이 false 면 아무것도 렌더링하지 않기
return (
<CommentBlock
value={value}
/>
);
}
}
export default connect(
(state, ownProps) => ({
// ownProps 에는 이 컴포넌트가 부모 컴포넌트에게서 받을 props 를 가르킵니다.
// 전달받는 post 를 기준으로, 해당 comments Map 의 주어진 post 의 _id 키를 가진 상태값을 가져옵니다
status: state.posts.getIn(['comments', ownProps.post.get('_id')])
})
)(CommentBlockContainer);
현재는 아직 우리가 컴포넌트에 post 값을 전달해주지 않아서 에러가 발생하게 됩니다. Post 컴포넌트에서 CommentBlockContainer 를 렌더링 할 때 post 를 전달해주도록 설정하세요.
src/components/Shared/PostList/Post.js
- CommentBlockContainer 렌더링
<CommentBlockContainer post={post}/>
덧글버튼이 클릭 됐을 때 실행 할 함수 만들기
이제, PostListContainer 에서 덧글버튼이 클릭 됐을때 실행되는 함수인 handleCommentClick
메소드를 정의하고, 이를 PostList 에 전달해주세요.
src/containers/Shared/PostList/PostListContainer.js
(...)
class PostListContainer extends Component {
(...)
handleCommentClick = (postId) => {
const { PostsActions } = this.props;
PostsActions.toggleComment(postId);
}
render() {
const { data } = this.props;
const { handleToggleLike, handleCommentClick } = this;
return (
<PostList
posts={data}
onToggleLike={handleToggleLike}
onCommentClick={handleCommentClick}
/>
);
}
}
(...)
이제, 이 메소드를 PostFooter 까지 전달을 해주어야 하는데요, 이 때 수정해야 할 컴포넌트가 꽤 많으니 차근 차근 따라와주세요.
우선, PostList 부터 수정을 해줍시다. 각 Post 에 onCommentClick 을 전달하세요.
src/components/Shared/PostList/PostList.js
(...)
const PostList = ({posts, onToggleLike, onCommentClick}) => {
const postList = posts.map(
(post) => (
<Post key={post.get('_id')} post={post} onToggleLike={onToggleLike} onCommentClick={onCommentClick}/>
)
)
(...)
}
그 다음엔 Post 컴포넌트에서, _id 값을 넣어서 함수를 호출하도록, 함수를 감싸서 새 함수를 정의하세요. 그리고, 그 함수를 onCommentClick props 로 PostFooter 에 전달합니다.
src/components/Shared/PostList/Post.js
(...)
const Post = ({post, onToggleLike, onCommentClick}) =>{
(...)
const commentClick = () => commentClick(_id);
return (
<Wrapper>
(...)
<PostFooter likesCount={likesCount} liked={liked} onToggleLike={toggleLike} onCommentClick={commentClick}/>
<CommentBlockContainer post={post}/>
</Wrapper>
)
}
마지막으로 PostFooter 에서 전달받은 onCommentClick 을, CommentIcon 이 클릭 됄 때 실행되도록 onClick 값으로 설정하세요.
src/components/Shared/PostList/PostFooter.js
(...)
const PostFooter = ({liked, likesCount=0, comments=[], onToggleLike, onCommentClick}) => (
<Wrapper>
<Likes active={liked}>
<HeartIcon onClick={onToggleLike}/>
<span>좋아요 {likesCount}개</span>
</Likes>
<Comments>
<CommentIcon onClick={onCommentClick}/>
<span>덧글 {comments.length}개</span>
</Comments>
</Wrapper>
);
export default PostFooter;
여기까지 하면, 덧글버튼을 눌렀을때, 덧글 창이 나타났다가, 사라집니다. 하지만, 레이아웃은 업데이트 되지 않아서 다음과 같은 현상이 발생하게 됩니다.
이렇게, 카드가 겹치는 현상이 발생하게되는데요, 이를 해결하려면, Masonry 컴포넌트에 ref 를 달아주고, masonry 의 layout()
함수를 실행시켜주어야합니다.
PostList 에서 Masonry 에 ref 달아주기
Masonry 컴포넌트를 렌더링 하게 될 때, ref 를 설정하겠습니다. 이 때, ref 값으로 전달되는 함수는 PostListContainer 에서 정의하여 받아오도록 합니다.
src/components/Shared/PostList/PostList.js
(...)
const PostList = ({posts, onToggleLike, onCommentClick, masonryRef}) => {
const postList = posts.map(
(post) => (
<Post key={post.get('_id')} post={post} onToggleLike={onToggleLike} onCommentClick={onCommentClick}/>
)
)
return (
<Wrapper>
<Masonry options={{gutter: 16}} ref={masonryRef}>
{postList}
</Masonry>
</Wrapper>
);
}
export default PostList;
그 다음엔, PostListContainer 에서 PostList 에 masonryRef 를 설정해주고, handleCommentClick 메소드가 실행 될 때, 우리가 만든 ref를 사용하여 layout()
을 호출하도록 작성하세요. 이 때, this.masonry
가 아닌 this.masonry.masonry
입니다. 그 이유는, 여기서 this.masonry
는 Masonry 컴포넌트를 가르키고, this.masonry.masonry
가, 실제 masonry 라이브러리 인스턴스를 가르키기 때문입니다.
이 함수를 호출하게 될 때, setTimeout(() => this.masonry.masonry.layout(), 0)
이런식으로 setTimeout 으로 감싸주어야, 다른 이벤트루프에서 작동하기 때문에, 덧글창이 열리고 나서 함수가 호출되게 됩니다. 만약에 setTimeout 을 하지 않으면 레이아웃이 toggleComment 작업이 마치기전에 실행되어 리레이아웃이 한발짝 늦게 처리되는것처럼 나타나게 됩니다.
src/containers/Shared/PostList/PostListContainer.js
(...)
class PostListContainer extends Component {
(...)
handleCommentClick = (postId) => {
const { PostsActions } = this.props;
PostsActions.toggleComment(postId);
setTimeout(() => this.masonry.masonry.layout(), 0);
}
render() {
const { data } = this.props;
const { handleToggleLike, handleCommentClick } = this;
return (
<PostList
posts={data}
onToggleLike={handleToggleLike}
onCommentClick={handleCommentClick}
masonryRef={ref => this.masonry = ref}
/>
);
}
}
(...)
이제 덧글 창 토글 기능이 완료되었습니다.