React로 SNS만들기 03-3 Redux 연동하기

onezeun·2022년 3월 1일

React로 SNS만들기

목록 보기
8/11

6. 더미데이터와 포스트폼 만들기

reducers/post.js


export const initialState = {
  mainPosts: [{···
  }],
  imagePaths: [], // 이미지 경로 저장
  postAdded: false, // 게시글 추가 완료여부
}

// 게시글 작성
const ADD_POST = 'ADD_POST'; // 변수로 지정해 주면 오타 났을 경우 잘 알 수 있음
export const addPost = {
  type: ADD_POST,
}

// 가짜 객체
const dummyPost = {
  id: 2,
  content: '더미데이터입니다',
  User: {
    id: 1,
    nickname: 'onezeun',
  },
  Images: [],
  Comments: [],
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_POST:
      return {
        ...state,
        mainPosts: [dummyPost, ...state.mainPosts],
        postAdded: true,
      }
    default:
      return state;
  }
}

export default reducer;

pages/index.js


import React from 'react';
import { useSelector } from 'react-redux';

import AppLayout from '../components/AppLayout';
import PostForm from '../components/PostForm';
import PostCard from '../components/PostCard';

const Home = () => {
  const { isLoggedIn } = useSelector((state) => state.user);
  const { mainPosts } = useSelector((state) => state.post);
  return (
    <AppLayout>
      {isLoggedIn && <PostForm />}
      {mainPosts.map((post) => <PostCard key={post.id} post={post} />)}
    </AppLayout>
  );
};

export default Home;

components/PostForm.js



import React, { useState, useCallback, useRef } from 'react';
import { Button, Form, Input } from 'antd';
import { useDispatch, useSelector } from 'react-redux';

import { addPost } from '../reducers/post';

const PostForm = () => {
  const { imagePaths } = useSelector((state) => state.post);
  const dispatch = useDispatch();
  const imageInput = useRef();
  
  const [text, setText] = useState('');
  const onChangeText = useCallback ((e) => {
    setText(e.target.value);
  }, []);
  
  const onSubmit = useCallback(() => {
    dispatch(addPost);
    setText('');
  }, []);
  
  const onClickImageUpload = useCallback(() => {
    imageInput.current.click();
  }, [imageInput.current]);

  return (
    <Form
      style={{ margin: '10px 0 20px' }}
      encType="multipart/from-data"
      onFinish={onSubmit}
    >
      <Input.TextArea
        value={text}
        onChange={onChangeText}
        maxLength={140}
        placeholder="어떤 신기한 일이 있었나요?"
      />
      <div>
        <input type="file" multiple hidden ref={imageInput} />
        <Button onClick={onClickImageUpload}>이미지 업로드</Button>
        <Button type="primary" style={{ float: 'right' }} htmlType="submit">
          짹짹
        </Button>
      </div>
      <div>
        {imagePaths.map((v) => (
          <div key={v} style={{ display: 'inline-block' }}>
            <img src={v} style={{ width: '200px' }} alt={v} />
            <div>
              <Button>제거</Button>
            </div>
          </div>
        ))}
      </div>
    </Form>
  );
};

export default PostForm;

7. 게시글 구현하기

components/PostCard.js

import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Card, Popover } from 'antd';
import { RetweetOutlined, HeartOutlined, MessageOutlined, EllipsisOutlined, HeartTwoTone } from '@ant-design/icons';
import ButtonGroup from 'antd/lib/button/button-group';
import { useSelector } from 'react-redux';
import Avatar from 'antd/lib/avatar/avatar';

import PostImages from './PostImages';

const PostCard = ({ post }) => {
  const [liked, setLiked] = useState(false);
  const [commentFormOpened, setCommentFormOpened] = useState(false);

  const onToggleLike = useCallback(() => {
    // false는 true로 true는 false로 (이전데이터를 기반으로 다음 데이터를 만듦)
    setLiked((prev) => !prev);  
  }, []);
  const onToggleComment = useCallback(() => {
    setCommentFormOpened((prev) => !prev);
  }, []);

  const id = useSelector((state) => state.user.me?.id);
  return (
    <div style={{ marginBottom: 20 }}>
      <Card
        cover={post.Images[0] && <PostImages images={post.Images} />}
        // 배열 안에는 항상 key를 넣어줘야 함
        actions={[
          <RetweetOutlined key="retweet" />,
          liked
          ? <HeartTwoTone twoToneColor="#eb2f96" key="heart" onClick={onToggleLike} />
          : <HeartOutlined key="heart" onClick={onToggleLike} />,
          <MessageOutlined key="comment" onClick={onToggleComment} />,
          <Popover
            key="more"
            content={
              <ButtonGroup>
                {/* 내 ID와 작성자 ID가 같을때 수정, 삭제 가능 다르면 신고 가능 */}
                {id && post.User.id === id ? (
                  <>
                    <Button>수정</Button>
                    <Button type="danger">삭제</Button>
                  </>
                ) : (
                  <Button>신고</Button>
                )}
              </ButtonGroup>
            }
          >
            <EllipsisOutlined />
          </Popover>,
        ]}
      >
        {/* 게시글 부분 */}
        <Card.Meta
          avatar={<Avatar>{post.User.nickname[0]}</Avatar>}
          title={post.User.nickname}
          description={post.content}
        />
        <Button></Button>
      </Card>
      {commentFormOpened && (
        <div>
          댓글부분
        </div>
      )}
      {/*
      <CommentForm />
      <Comments />
       */}
    </div>
  );
};



PostCard.propTypes = {
  // 더 자세하게 작성하려면 shape을 쓰고 안에 속성을 넣어주면 됨
  post: PropTypes.shape({
    id: PropTypes.number,
    user: PropTypes.object,
    content: PropTypes.string,
    createdAt: PropTypes.object,
    Comments: PropTypes.arrayOf(PropTypes.object),
    Images: PropTypes.arrayOf(PropTypes.object),
  }).isRequired,
};

export default PostCard;

옵셔널 체이닝(optional chaining) 연산자

옵셔널이 연달아 호출된 것을 의미

const { me } = useSelector((state) => state.user);
const id = me?.id;

// 옵셔널 체이닝을 안쓰면?
const id = me && me.id

한번에 쓰기

const id = useSelector((state) => state.user.me?.id);

PropTypes.shape

PropTypes와 함께 하는 타입 검사

8. 댓글 구현하기

components/PostCard.js


...
{commentFormOpened && (
  <div>

    <CommentForm post={post}/>
    <List
    header={`${post.Comments.length}개의 댓글`}
    itemLayout="horizontal"
    dataSource={post.Comments}
    renderItem={(item) => (
      <li>
        <Comment
        author={item.User.nickname}
        avatar={<Avatar>{item.User.nickname[0]}</Avatar>}
        content={item.content}
        />
      </li>
    )}
    />          
  </div>
)}

CommentFormpost를 넘겨주는 이유

어떤 게시글에 댓글을 달건지 정보가 필요하기 때문
게시글의 id를 받아야 함


components/CommentForm.js


import React, { useCallback } from 'react';
import PropTypes from 'prop-types'
import { Button, Form, Input } from 'antd';

import useInput from '../hooks/useInput';
import { useSelector } from 'react-redux';



const CommentForm = ({ post }) => {
  const id = useSelector((state) => state.user.me?.id)
  const [commentText,onChangeCommentText] = useInput('');
  const onSubmitComment = useCallback(() => {
    console.log(post.id, commentText)
  }, [commentText]);

  return (
    <Form onFinish={onSubmitComment}>
      <Form.Item style={{ position: 'relative', margin: 0 }}>
        <Input.TextArea value={commentText} onChange={onChangeCommentText} rows={4} />
        <Button style={{ position: 'absolute', right: 0, bottom: -40 }} type="primary" htmlType='submit'>삐약</Button>
      </Form.Item>
    </Form>
  );
};

CommentForm.propTypes = {
  post: PropTypes.object.isRequired,
}

export default CommentForm;

📚 참고
Swift) Optional 부수기 (6) - Optional Chaining (옵셔널 체이닝)

profile
엉망진창

0개의 댓글