React - Notion (2)

김정욱·2020년 11월 28일
0

React

목록 보기
20/22
post-thumbnail
post-custom-banner

[ 구현 사항 ]

1) / , /members, /members/:id 페이지 라우팅 설정
2) Grid를 이용한 Card배치
3) 데이터 로딩중 설정
4) 게시글 CRUD(Create / Read / Update / Delete)

Create

: 내용이 비어있는 Card를 추가하는 기능


(MemberList.js)

...
    /* 빈 카드를 클릭시 발생하는 함수 */
    const onCreateCard = async () => {
      /* 빈 내용을 담는 object 생성 */
        const object = {
            "name": "",
            "profileUrl": "",
            "introduction": "",
            "mbti": "",
            "instagram": ""
        };
        try{
           /* 서버에 빈 객체를 생성하는 api요청 */
            const result = await createMember(object);
          /* 성공시에 반환된 빈 내용을 현재 members에 추가! */
            setMembersState({
                members: [...membersState.members, result],
                status: 'resolved'
            });
        }catch(e){
            setMembersState({members: membersState.members, status: 'rejected'});
        }
    }
...
          <div className="member-list-content-wrapper">
              {membersState.members.map((member, i) =>
                  <Card key={"card-" + i} 
                      memberData={member} onRemoveCard={removeCard} />)}
                      /* CardEmpty 클릭시 새로운 카드 생성 */
              <CardEmpty onClick={onCreateCard}>
                  <CardEmptyText>+ New</CardEmptyText>
              </CardEmpty>
          </div>
...

: members.map을 통해 여러 Card를 출력하는 마지막에 빈 카드 추가!
  그리고 onCreateCard라는 이벤트 처리 함수에서 서버와 통신


(memberAPI.js)

...
const createMember = async(object) => {
    try {
        const { data } = await axios.post(`${url}`,object);
        console.log('[SUCCESS] CREATE MEMBER', data);
        return data.data;
    } catch (e) {
        console.error('[FAIL] CREATE MEMBER', e);
        throw(e);
    }
}

export {
    getMembers,
    createMember,
};

: 넘겨받은 빈 객체를 서버에 보내서 생성하는 코드


[ 결과물 ]

Read

: 특정 Card클릭시 MemberDetail로 이동해서 자세한 해당 내용을 보여주는 기능


(Card.js)

import './Card.scss';
import { DeleteOutlined, FileImageOutlined } from '@ant-design/icons';
import {withRouter} from 'react-router-dom';

function Card({ history, match, memberData }) {

    return (
      /* onClick으로 클릭할 때 /members/:id로 이동하게 한다.  */
        <div className="card" onClick={() => history.push(`${match.path}/${memberData.id}`)} draggable >
            <div className="remove-button" onClick>
                <DeleteOutlined style={{ fontSize: "16px"}}/>
            </div>
            <div className="image-area">
                { memberData.profileUrl !== '' ? <img src={memberData.profileUrl} alt="profile"></img> : <FileImageOutlined style={{fontSize: "40px"}}/> }
            </div>
            <div className="card__content card__text name">{memberData.name}</div>
            <div className="card__content card__text instagram">{memberData.instagram}</div>
            <div className="card__content card__text introduction">{memberData.introduction}</div>
            {
                memberData.mbti &&
                <span className="card__content card__text mbti">{memberData.mbti}</span>
            }
        </div>
    );
}

export default withRouter(Card);

: 해당 컴포넌트는 직접 Route되지 않았기 때문withRouter를 사용하여
  history와 match를 가져와서 Redirect하였다


(MemberDetail.js)

import React, {useState, useEffect} from 'react'
import { getMember, updateMember } from '../../lib/api/memberAPI';
import Button from '../../components/button/Button';
import Loading from '../../components/loading/Loading';
import { Input } from 'antd';
import { InstagramOutlined, AlignLeftOutlined, RadarChartOutlined } from '@ant-design/icons';
import './MemberDetail.scss'

function MemberDetail({match}) {
    const [ memberState, setMemberState ] = useState({
        status: 'idle',
        member: null
    });

    /* Component가 mount되었을 때 id에 해당하는 정보를 서버에서 받아오는 코드 */
    useEffect(() => {
        (async () => {
            try {
                setMemberState({ status: 'pending', member: null });
              /* url에서 params인 id를 빼서 getMember에 넘겨준다 */
                const result = await getMember(match.params.id);
              /* setTimeout을 쓴 이유는 그냥 로딩중이 너무 빨리지나가서 아쉬워서; */
                setTimeout(() => setMemberState({ status: 'resolved', member: result }), 800);
            } catch (e) {
                setMemberState({ status: 'rejected', member: null });
            }
        })();
    }, []);

  /* 역시 MemberList.js처럼 조건부 렌더링을 해서 Loading중 처리! */
    switch (memberState.status) {
        case 'pending':
            return <Loading />;
        case 'rejected':
            return <div>데이터 로드 실패!</div>
        case 'resolved':
            return (
                <>
                    <div className="member-detail">
                        <div className="member-detail__button-area">
                            <Button text="Add icon"></Button>
                            <Button text="Add cover"></Button>
                        </div>
                            <Input className="member-detail__content name" bordered={false} name="name" value={memberState.member.name} onChange/>
                        <hr style={{borderTop: "solid 1px #eee", marginBottom: "24px"}}/>
                        <div className="member-detail__content">
                            <div className="content-title"><InstagramOutlined />&nbsp; 인스타 아이디</div>
                            <Input className="content-input" bordered={false} name="instagram" value={memberState.member.instagram} onChange/>
                        </div>
                        <div className="member-detail__content">
                            <div className="content-title"><AlignLeftOutlined />&nbsp; 한 줄 소개</div>
                            <Input className="content-input oneline" bordered={false} name="introduction" value={memberState.member.introduction} onChange/>
                        </div>
                        <div className="member-detail__content">
                            <div className="content-title"><RadarChartOutlined />&nbsp; mbti</div>
                            <Input className="content-input" bordered={false} name="mbti" value={memberState.member.mbti} onChange/>
                        </div>
                        <div className="member-detail__content">
                            { memberState.member.profileUrl !== '' ? <img className="content-image" src={memberState.member.profileUrl} alt={'profile url'} /> : '' }
                        </div>
                    </div>
                </>
            );
        case 'idle':
        default :
            return <div>idle 입니다</div>
    }
}

export default MemberDetail

: match.params를 통해서 url에 있는 id라는 params를 가져온 뒤
  서버와 통신하여 해당 정보를 저장하고 출력


(memberAPI.js)

...
const getMember = async (id) => {
    try {
        const { data } = await axios.get(`${url}/${id}`);
        console.log('[SUCCESS] GET MEMBER', data);
        return data.data;
    } catch (e) {
        console.error('[FAIL] GET MEMBER', e);
        throw(e);
    }
}

export {
    getMembers,
    getMember,
    createMember,
};

: 전달받은 id를 통해 서버에서 정보를 받아온 뒤 response!


[ 결과물 ]

Update

: MemberDetail 페이지에서 각 정보를 클릭하여 수정하면 바로바로 서버와 통신
  내용이 저장되는 기능


(MemberDetail.js)

...
/* onChange 이벤트 함수 작성 */
const onChangeInputs = async (evt) =>{
  /* input태그이기 때문에 바로 evt.target에서 name을 가져올 수 있다
  div였다면 evt.target.attributes에서 가져와야 했을 것이다 */
    const {name, value} = evt.target;
    const result = await updateMember(match.params.id, {
        ...memberState.member,
      /* 이 부분으로 name과 value를 각각 매칭 해주어서 효율적인 코드가 가능한 것! */
        [name]: value
    });
    setMemberState({
        status: 'resolved',
        member: result
    });
}

switch (memberState.status) {
  case 'pending':
      return <Loading />;
  case 'rejected':
      return <div>데이터 로드 실패!</div>
  case 'resolved':
      return (
      <>
          <div className="member-detail">
              <div className="member-detail__button-area">
                  <Button text="Add icon"></Button>
                  <Button text="Add cover"></Button>
              </div>
                  <Input className="member-detail__content name" bordered={false} name="name" value={memberState.member.name} onChange={onChangeInputs}/>
              <hr style={{borderTop: "solid 1px #eee", marginBottom: "24px"}}/>
              <div className="member-detail__content">
                  <div className="content-title"><InstagramOutlined />&nbsp; 인스타 아이디</div>
                  <Input className="content-input" bordered={false} name="instagram" value={memberState.member.instagram} onChange={onChangeInputs}/>
              </div>
              <div className="member-detail__content">
                  <div className="content-title"><AlignLeftOutlined />&nbsp; 한 줄 소개</div>
                  <Input className="content-input oneline" bordered={false} name="introduction" value={memberState.member.introduction} onChange={onChangeInputs}/>
              </div>
              <div className="member-detail__content">
                  <div className="content-title"><RadarChartOutlined />&nbsp; mbti</div>
                  <Input className="content-input" bordered={false} name="mbti" value={memberState.member.mbti} onChange={onChangeInputs}/>
              </div>
              <div className="member-detail__content">
                  { memberState.member.profileUrl !== '' ? <img className="content-image" src={memberState.member.profileUrl} alt={'profile url'} /> : '' }
              </div>
          </div>
      </>
      );
  case 'idle':
  default :
      return <div>idle 입니다</div>
}
}

: name / instagram / introduction / mbti 에 onChange를 작성!
  각 요소가 바뀔 때 마다 서버와 통신하여 값을 저장한다
  (input태그여서 evt.target에서 name을 뺄 수 있음
  / div였으면 e.target.attributes에서 빼야함)


(memberAPI.js)

...
const updateMember = async (id, object) => {
    try {
        const { data } = await axios.put(`${url}/${id}`,object);
        console.log('[SUCCESS] UPDATE MEMBER', data);
        return data.data;
    } catch (e) {
        console.error('[FAIL] UPDATE MEMBER', e);
        throw(e);
    }
}

export {
    getMembers,
    getMember,
    createMember,
    updateMember,
};

[ 결과물 ]

Delete

: 카드 hover했을 때 뜨는 휴지통 모양 클릭시 Card삭제하는 기능!


(Card.js)

import './Card.scss';
import { DeleteOutlined, FileImageOutlined } from '@ant-design/icons';
import {withRouter} from 'react-router-dom';
import {removeMember} from '../../lib/api/memberAPI';

function Card({ history, match, memberData, onRemoveCard }) {

    const onClickRemove = async (evt) => {
        /* 버블링을 막아서 클릭 이벤트 전파를 막아야 상위 요소 클릭이 눌리지 않는다 */
        evt.stopPropagation();
        try{
            await removeMember(memberData.id);
            onRemoveCard(memberData.id);
        }catch(e){
            // 오류 처리
            console.log(e);
        }
    }
    return (
        <div className="card" onClick={() => history.push(`${match.path}/${memberData.id}`)} draggable >
            <div className="remove-button" onClick={onClickRemove}>
                <DeleteOutlined style={{ fontSize: "16px"}}/>
            </div>
            <div className="image-area">
                { memberData.profileUrl !== '' ? <img src={memberData.profileUrl} alt="profile"></img> : <FileImageOutlined style={{fontSize: "40px"}}/> }
            </div>
            <div className="card__content card__text name">{memberData.name}</div>
            <div className="card__content card__text instagram">{memberData.instagram}</div>
            <div className="card__content card__text introduction">{memberData.introduction}</div>
            {
                memberData.mbti &&
                <span className="card__content card__text mbti">{memberData.mbti}</span>
            }
        </div>
    );
}

export default withRouter(Card);

: remove-button클릭시 onClickRemove라는 이벤트 처리함수를 작성

    const onClickRemove = async (evt) => {
        /* 버블링을 막아서 클릭 이벤트 전파를 막아야 상위 요소 클릭이 눌리지 않는다 */
        evt.stopPropagation();
        try{
          /* 해당 id를 가진 데이터를 삭제하기 위해 removeMember에 전달 */
            await removeMember(memberData.id);
          /* MemberList에서 받은 onRemoveCard로 전체 데이터에서 삭제해줘야 함 */
            onRemoveCard(memberData.id);
        }catch(e){
            // 오류 처리
            console.log(e);
        }
    }

: 주목할 점은 바로 삭제 api를 통해 삭제 하면 서버에는 저장되지만 다시 값을 받아오지
  않는 이상 우리의 메인 MemberList는 알지 못하다는 것
이다.
 따라서 MemberList에서 onRemoveCard라는 함수를 넘겨서 해당 처리를 해주어야 한다!


(MemberList.js)

...
    const removeCard = (id) => {
        setMembersState({
            members: membersState.members.filter(mem => mem.id !== id),
            status: 'resolved'
        });
    };
...
      <div className="member-list-content-wrapper">
          {membersState.members.map((member, i) =>
              <Card key={"card-" + i} 
                  memberData={member} onRemoveCard={removeCard} />)}
          <CardEmpty onClick={onCreateCard}>
              <CardEmptyText>+ New</CardEmptyText>
          </CardEmpty>
      </div>

: 삭제한 id를 받아서 현재 members에서 filter로 삭제해주어야
  Front에서도 다시 렌더링하여 삭제 처리를 바로 확인할 수 있다


(memberAPI.js)

...
const removeMember = async(id) => {
    try {
        const { data } = await axios.delete(`${url}/${id}`);
        console.log('[SUCCESS] DELETE MEMBER', data);
        return data.data;
    } catch (e) {
        console.error('[FAIL] DELETE MEMBER', e);
        throw(e);
    }
}

export {
    getMembers,
    getMember,
    createMember,
    removeMember,
    updateMember,
};

[ 결과물 ]

profile
Developer & PhotoGrapher
post-custom-banner

0개의 댓글