본문 바로가기
React

2021.09.24 React Post Create

by 해맑은 코린이 2021. 9. 24.

2021.09.24 React Post Create_정리노트

 

다음주까지 완성하기로...스터디원들과 약속했돠..... 그리고...10월달까지 미루지 않고 완독을 해내리라....후 말이 create 지 쉽게 말하면 글쓰기 기능 꼬꼬

 

바로 빡세게 달려보자!!!!

 

회원가입, 로그인과 같이 비슷하게 UI 를 구현하고, 리덕스로 상태관리를 하고 마지막으로는 API 연동하기 까지!!! 

 

이번에는 정리하며 주절주절 써보자!!!!

 

 

먼저 글쓰기는 Quill 이라는 에디터 라이브러리를 사용해서 구현할거기 때문에

$ yarn add quill

라이브러리 먼저 설치하고 시작!

 

 

먼저 라이브러리를 설치했으니까 에디터 UI 부터 만들기!

 

src/components/wirte/Editor.js (새로 생성)

import React, { useEffect, useRef } from 'react';
// editor import
import Quill from 'quill';
import 'quill/dist/quill.bubble.css';


// styled-component import
import styled from 'styled-components';

// pallet import
import palette from '../../lib/styles/palette';

// responsive component import
import Responsive from '../common/Responsive';

const EditorBlock = styled(Responsive)`
  /* 페이지 위아래 여백 지정 */
  padding-top: 5rem;
  padding-bottom: 5rem;
`;

const TitleInput = styled.input`
  font-size: 3rem;
  outline: none;
  padding-bottom: 0.5rem;
  border: none;
  border-bottom: 1px solid ${palette.gray[4]};
  margin-bottom: 2rem;
  width: 100%;
`;


const QuillWrapper = styled.div`
    .ql-editor{
        padding:0;
        min-height: 320px;
        font-size: 1.125rem;
        line-height: 1.5;
    }
/* 첫번째 자식요소에 스타일링 css */
    .ql-editor .ql-blank::before{
        left: 0;
    }
`;
const Editor = () => {
    // quill 적용 div element 를 설정
    const quillElement = useRef(null);
    // quill instance 설정
    const quillInstance = useRef(null);

    useEffect(()=>{
        // useRef 로 DOM 요소에 접근하려면 .current 사용
        quillInstance.current = new Quill(quillElement.current,{
            // 테마 snow, bubble 두 가지가 있으며 불러올 때  'quill/dist/quill.snow.css', 'quill/dist/quill.bubble.css' 두 가지중 골라서 사용
            theme:'bubble',
            placeholder:'내용을 작성하세요..',
            modules:{
                toolbar:[
                    // toolbar option
                    [{header : '1'},{header: '2'}], //custom button values
                    // strike : 글씨에 밑줄
                    ['bold','italic','underline','strike','link'] , //toggle btn option
                    [{list: 'ordered'},{list:'bullet'}], // list option
                    ['blockquote','code-block','link','image'], // toggle btn option 
                ]
            }
        });
    },[]);
  return (
  
  <EditorBlock>
      <TitleInput placeholder="제목을 입력하세요.." />
      <QuillWrapper>
          {/* 컴포넌트 내부의 DOM 을 외부에서도 사용하기 위해 컴포넌트에 직접 ref 전달 == DOM 에 ref 를 다는 것과 같음 */}
          <div ref={quillElement} />
      </QuillWrapper>
  </EditorBlock>
  
  
  );
};

export default Editor;

 

자세한 옵션은 

https://quilljs.com/docs/modules/toolbar

 

Toolbar Module - Quill

The Toolbar module allow users to easily format Quill’s contents. It can be configured with a custom container and handlers. var quill = new Quill('#editor', { modules: { toolbar: { container: '#toolbar', // Selector for toolbar container handlers: { 'bo

quilljs.com

 

요기서 알 수 있음!

 

 

테마 또한 

 

https://quilljs.com/docs/themes/

 

Themes - Quill

Themes Themes allow you to easily make your editor look good with minimal effort. Quill features two offically supported themes: Snow and Bubble. Usage var quill = new Quill('#editor', { theme: 'bubble' // Specify theme in configuration }); Bubble Bubble i

quilljs.com

 

여기서 가져올 수 있으니, 두 가지중에 맘에 드는 테마를 css 와 함께 가져오면 된다!

 

나는 버블 테마를 사용해서,

 

 

실행 화면 

http://localhost:3000/write

 

이렇게 화면이 나타나게 된다!

 

갸륵 정말 많은 기능들이 옵션으로 들어가는군 얘들을 나중에 리덕스에서 상태 관리를 할 수 있게 props 로 설정해주는 부분은 나중에 뒤에서 할 것!

 

 

이제 티스토리처럼 태그를 추가해주는 태그박스랑 포스트 작성을 완료하거나 취소하는 것도 UI 만들어서 기능까지 한번에 정리해보자! 

 

 

src/components/write/TagBox.js ( 새로 생성 )

 

// useState,useEffect import
import React, { useCallback, useState } from 'react';
// styled-components import
import styled from 'styled-components';
// palette import
import palette from '../../lib/styles/palette';

const TagBoxBlock = styled.div`
  width: 100%;
  border-top: 1px solid ${palette.gray[2]};
  padding-top: 2rem;

  h4 {
    color: ${palette.gray[8]};
    margin-top: 0;
    margin-bottom: 0.5rem;
  }
`;

const TagForm = styled.form`
  border-radius: 4px;
  /* 넘치는 form hidden */
  overflow: hidden;
  display: flex;
  width: 256px;
  /* style 초기화 ( 위에서 준 border-top style ) */
  border: 1px solid ${palette.gray[9]};
  /* input, button에 똑같은 style 주기 */
  input,
  button {
    outline: none;
    border: none;
    font-size: 1rem;
  }

  input {
    padding: 0.5rem;
    flex: 1;
    min-width: 0;
  }
  button {
    cursor: pointer;
    padding-right: 1rem;
    padding-left: 1rem;
    border: none;
    background: ${palette.gray[8]};
    color: white;
    font-weight: bold;
    &:hover {
      background: ${palette.gray[6]};
    }
  }
`;

const Tag = styled.div`
  margin-right: 0.5rem;
  color: ${palette.gray[6]};
  cursor: pointer;
  &:hover {
    opacity: 0.5;
  }
`;

const TagListBlock = styled.div`
/* 태그들을 가로로 정렬 */
  display: flex;
  margin-top: 0.5rem;
`;

// 렌더링을ㄹ 최적화하기 위해 TagItem ,TagList 로 두 가지 컴포넌트 분리. 만약 한 컴포넌트에서 직접 렌더링시  input 값이 바뀔 때 태그의 목록도 리렌더링됨. 또 태그 목록이 리렌더링 되면 태그하나하나 모두 리렌더링

// React.memo 사용으로 tag 값이(props 가 실제로 바뀔 때만) 바뀔 때만 리렌더링 되도록 처리
const TagItem = React.memo(({ tag, onRemove }) => (
  // tag 를 받아서 렌더링
  // 태그를 클릭할 때 onRemove 함수 실행
  <Tag onClick={() => onRemove(tag)}>#{tag}</Tag>
));

// 마찬가지로 React.memo 사용해서 tags 값이 바뀔 때만 리렌더링 되도록 처리
const TagList = React.memo(({ tags, onRemove }) => (
  <TagListBlock>
    {tags.map((tag) => (
      //  map 을 사용하여 컴포넌트로 변환할 때는 key 값 필요. tag 값을 key 값으로 설정
      // tag 데이터를 통째로 props 로 전달할 거임. 객체 통째로 전달해주는게 나중에 여러 종류의 값을 전달해야 하는 경우 최적화가 편함.
      // onRemove 함수 props
      <TagItem key={tag} tag={tag} onRemove={onRemove} />
    ))}
  </TagListBlock>
));

const TagBox = () => {
  // input, tags state
  const [input, setInput] = useState('');
  const [localTags, setLocalTags] = useState([]);

//   해당 2번째 인자로 받은 state 값들이 바뀔 때마다만 렌더링 해줘야 최적화기 떄문에 useCallback 사용


  const insertTag = useCallback(
    //   submit 했을 때 해당 함수 실행
    (tag) => {
      if (!tag) return; // 공백이면 추가하지 않음.
      if (localTags.includes(tag)) return; // 이미 존재하면 추가하지 않음.
    //   state 에서는 배열, 객체를 바꿔줄 때 불변성 유지를 위해 spread 연산자를 사용해서 값을 업데이트 해야함.
      setLocalTags([...localTags, tag]);
    },
    [localTags],
  );

  const onRemove = useCallback(
    (tag) => {
        // 클릭한 해당 요소만 빼고 새로운 배열 반환하는 filter 함수
      setLocalTags(localTags.filter((t) => t !== tag));
    },
    [localTags],
  );

  const onChange = useCallback((e) => {
      //input onchange event 로 target value 를 상태값으로 업데이트
    setInput(e.target.value);
  }, []);

  const onSubmit = useCallback(
    (e) => {
      console.log(e);
      e.preventDefault();
    //  setInput 으로 업데이트된 값이 input 에 담겨져있을테니 얘를 앞뒤의 공백을 없애는 trim을 써서 tag 값 업데이트
      insertTag(input.trim()); 
      // input 초기화
      setInput('');
    },
    [input, insertTag],
  );
  return (
    <TagBoxBlock>
      <h4>태그</h4>
      <TagForm onSubmit={onSubmit}>
        <input
          placeholder="태그를 입력하세요"
          value={input}
          onChange={onChange}
        />
        <button type="submit">글 추가</button>
      </TagForm>
      {/* 업데이트 된 state 값을 tags props로, 클릭했을 때 onRemove 함수 실행 */}
      <TagList tags={localTags} onRemove={onRemove} />
    </TagBoxBlock>
  );
};

export default TagBox;

 

개ㅐㅐㅐ길죠..? ㅋㅋㅋㅋㅋㅋㅋㅋㅋ 후 뭔가 UI 만 따로 빼기엔 애매하고... 흠 기능들을 하나하나 혼자서 정리하다보니 너무 긴걸.... 껄껄

코드 복붙해서 주석이랑 같이 흐름..정리해보는걸 추천 나도 혼자서 왔다리갔다리 겁나게 정리했음.. 이리저리 함수도 많고 컴포넌트도 연결된게 많고 하다보니..껄껄 진짜 혼자 위아래로 왔다갔다하면서 난리남ㅋㅋㅋㅋ 그리고 그동안 배웠던 개념들 같은 것도 혼자 정리하다보니 많다많아 다 찾아봄.. 오랜만에 보니까 useState 도 까먹은게 좀 많더라궁

 

자 얘를 이제 WritePage 하단에 렌더링!

// create
import React from 'react';
import Responsive from '../components/common/Responsive';
import Editor from '../components/write/Editor';
import TagBox from '../components/write/TagBox';

const WritePage = () => {
  return (
    <div>
      <Responsive>
        <Editor />
        {/* 추가 */}
        <TagBox />
      </Responsive>
    </div>
  );
};

export default WritePage;

 

그러고

실행 화면

 

이렇게 뜨고 

 

 

 

공백이 없게 태그가 잘 들어가면 성공임쓰

 

 

그리고 해당 태그를 눌렀을 때

 

 

없어지면 성공~ 

 

짝짝 기능은 다 구현했으니 나중에 리덕스에서 상태 관리만 해주면 됨.

 

 

또또 컴포넌트 포스트 작성 및 취소도 해야겠졍~ 가즈앙

 

얘는 버튼 UI 만 만들어주고 상태는 리덕스에서 관리할 거기 때문에 UI 부분만 일단 적어보자

 

 

src/components/write/WriteActionButton.js ( 새로 생성 )

import React from 'react';
import styled from 'styled-components';
// button component import
import Button from '../common/Button';

const WriteActionButtonBlock = styled.div`
  margin-top: 1rem;
  margin-bottom: 3rem;
  /* button 끼리 붙어있을 때의 style */
  button + button {
    margin-left: 0.5rem;
  }
`;

// button 컴포넌트를 가져와서 새 컴포넌트로 만듬
// tagBox 와 동일한 높이로 설정한 후 서로 간의 여백 지정
const StyledButton = styled(Button)`
  height: 2.125rem;
  & + & {
    margin-left: 0.5rem;
  }
`;
const WriteActionButton = ({ onCancel, onPublish }) => {
  return (
    <WriteActionButtonBlock>
        {/* click event props settings */}
      <StyledButton onClick={onPublish}>포스트 등록</StyledButton>
       {/* button color props,click event props settings */}
      <StyledButton gray onClick={onCancel}>취소</StyledButton>
    </WriteActionButtonBlock>
  );
};

export default WriteActionButton;

 

이렇게 스타일을 만들어주고, 미리 onPublish, onCancel 이라는 props 를 세팅해주었음.

참고로 gray props 는

요 버튼에 있는 props 임!

 

 

이제 얘를 WritePage 에서 렌더링!

 

src/pages/WritePage 

 

// create
import React from 'react';
import Responsive from '../components/common/Responsive';
import Editor from '../components/write/Editor';
import TagBox from '../components/write/TagBox';
import WriteActionButton from '../components/write/WriteActionButton';

const WritePage = () => {
  return (
    <div>
      <Responsive>
        <Editor />
        <TagBox />
        {/* 추가 */}
        <WriteActionButton />
      </Responsive>
    </div>
  );
};

export default WritePage;

 

실행 화면

 

 

깔-끔 이제 글쓰기 관련 상태..를 리덕스로 관리하고, API 호출할 차례..! 으으으으..화이텡...

 

리덕스부터 간다! 

먼저 모듈 작성!

 

src/modules/write.js ( 새로 생성 )

import { createAction, handleActions } from 'redux-actions';


// 액션 타입 정의

const INITIALIZE = 'write/INITIALIZE';
const CHANGE_FIELD = 'write/CHANGE_FIELD';


// 액션 생성 함수
export const initialize = createAction(INITIALIZE);
export const changeField = createAction(CHANGE_FIELD,({ key, value })=>({
    key,
    value
}));

// 초기 상태 정의
const initialState = {
    title:'',
    body:'',
    tags:[]
};


// 리듀서 함수

const write = handleActions(
    {
        [INITIALIZE]: state => initialState, // initialState 를 넣으면 초기 상태로 바뀜
        [CHANGE_FIELD] : (state,{payload: { key,value }}) =>({
            ...state,
            [key] : value, // 특정 key 값 업데이트
        })
    },
    initialState
)


export default write;

 

리듀서를 만들었으니 루트 리듀서 추가~~

 

import { combineReducers } from "redux";
// all import 
import { all } from 'redux-saga/effects';
import auth,{ authSaga } from "./auth";
// loading import
import loading from "./loading";
// user import
import user,{ userSaga } from "./user";
// write import
import write from "./write";

// write add
const rootReducer = combineReducers({
    auth,
    loading ,
    user,
    write
});

export function* rootSaga(){
    // all 은 배열안에 있는 모든 제너레이터 함수들이 병행적으로 동시에 실행되고, 전부 이행될 때까지 기다림.
    // Promise.all 과 비슷하다고 해서 찾아보니 모든 것들이 이행될 때까지 기다리고 하나라도 에러가 나면 모든 Promise는 무시가 되고, catch 문 실행한다니 이와 비슷할듯
    yield all([authSaga(),userSaga()]);
}

export default rootReducer;

 

 

그리고 아까 구현해놓은 Editor, TagBox, WriteActionButtons 컴포넌트에서 리덕스를 적용시킬 컨테이너 컴포넌트를 만들텐데, 책에서도 적혀있지만, 구현할 기능이 많지 않다면 그냥 하나의 컨테이너 컴포넌트에서 상태 관리를 해주어도 괜찮지만, 나중에 확장성을 위해서 코드가 방대해졌을 때의 유지보수 가능성을 생각해서 각각 그냥 분리시켜주는게 좋다고 하니 안그래도 헷갈리는 나에게는 좋은 방법 ㅎㅅㅎ

 

 

일단 에디터 컨테이너부터!

 

src/containers/write/EditorContainer.js ( 새로 생성 )

 

import React, { useCallback, useEffect } from 'react';
import Editor from '../../components/write/Editor';
import { useSelector, useDispatch } from 'react-redux';
import { changeField, initialize } from '../../modules/write';

const EditorContainer = () => {
  const dispatch = useDispatch();
  const { title, body } = useSelector(({ write }) => ({
    title: write.title,
    body: write.body,
  }));

//   useCallback ?? >> useEffect 에서 나중에 이 함수를 쓸 건데, useCallback 을 써야 에디터에서 사용할 때 컴포넌트가 화면에 나타났을 딱 그시점에 한번만 실행 되기 때문.
  const onChangeField = useCallback(
    (payload) => dispatch(changeField(payload)),
    [dispatch],
  );
//  다른 페이지를 갔다가 다시 이 페이지로 왔을 때는 내용 초기화. 
  useEffect(() => {
    return () => {
      dispatch(initialize());
    };
  }, [dispatch]);

  return (
    <Editor onChangeField={onChangeField} title={title} body={body}></Editor>
  );
};

export default EditorContainer;

 

여기서는 title,body 의 값이 리덕스 스토어에서 불러와서 에디터 컴포넌트에 props 로 전달해주었는데, quill 에디터를 사용해서 input,textarea 처럼 onChange event value 값으로 상태를 관리할 수 없다. 그래서 지금은 에디터에서 값이 바뀔 때 리덕스 스토어에 값을 넣는 작업만 하고, 리덕스 스토어의 값이 바뀔 때 에디터 값이 바뀌게 하는 작업은 나중에 포스트 업데이트에서 할 예정!!!

 

src/pages/WritePage

// create
import React from 'react';
import Responsive from '../components/common/Responsive';
// EditorContainer import
import EditorContainer from '../containers/write/EditorContainer';
import TagBox from '../components/write/TagBox';
import WriteActionButton from '../components/write/WriteActionButton';

const WritePage = () => {
  return (
    <div>
      <Responsive>
          {/* 교체 */}
        <EditorContainer />
        <TagBox />
        <WriteActionButton />
      </Responsive>
    </div>
  );
};

export default WritePage;

이제 컨테이너로 교체시켜주고, 에디터 컴포넌트 수정으로 간다!

 

components/write/Editor

import React, { useEffect, useRef } from 'react';
// editor import
import Quill from 'quill';
import 'quill/dist/quill.bubble.css';


// styled-component import
import styled from 'styled-components';

// pallet import
import palette from '../../lib/styles/palette';

// responsive component import
import Responsive from '../common/Responsive';

const EditorBlock = styled(Responsive)`
  /* 페이지 위아래 여백 지정 */
  padding-top: 5rem;
  padding-bottom: 5rem;
`;

const TitleInput = styled.input`
  font-size: 3rem;
  outline: none;
  padding-bottom: 0.5rem;
  border: none;
  border-bottom: 1px solid ${palette.gray[4]};
  margin-bottom: 2rem;
  width: 100%;
`;


const QuillWrapper = styled.div`
    .ql-editor{
        padding:0;
        min-height: 320px;
        font-size: 1.125rem;
        line-height: 1.5;
    }
/* 첫번째 자식요소에 스타일링 css */
    .ql-editor .ql-blank::before{
        left: 0;
    }
`;

// redux props
const Editor = ({ title, body, onChangeField }) => {
    // quill 적용 div element 를 설정
    const quillElement = useRef(null);
    // quill instance 설정
    const quillInstance = useRef(null);

    useEffect(()=>{
        // useRef 로 DOM 요소에 접근하려면 .current 사용
        quillInstance.current = new Quill(quillElement.current,{
            // 테마 snow, bubble 두 가지가 있으며 불러올 때  'quill/dist/quill.snow.css', 'quill/dist/quill.bubble.css' 두 가지중 골라서 사용
            theme:'bubble',
            placeholder:'내용을 작성하세요..',
            modules:{
                toolbar:[
                    // toolbar option
                    [{header : '1'},{header: '2'}], //custom button values
                    // strike : 글씨에 밑줄
                    ['bold','italic','underline','strike','link'] , //toggle btn option
                    [{list: 'ordered'},{list:'bullet'}], // list option
                    ['blockquote','code-block','link','image'], // toggle btn option 
                ]
            }
        });
        // text-change event handler
        const quill = quillInstance.current;
        quill.on('text-change', ( delta, oldDelta, source) =>{
            if (source === 'user'){
                onChangeField({ key : 'body' ,value: quill.root.innerHTML})
            }
        });
    },[onChangeField]);

    // input 은 e.target.value 로 설정
    const onChangeTitle = e =>{
        onChangeField({ key: 'title' , value : e.target.value})
    }

 
  return (
  
  <EditorBlock>
      <TitleInput 
      placeholder="제목을 입력하세요.."
      onChange={onChangeTitle}
      value={title}
       />
      <QuillWrapper>
          {/* 컴포넌트 내부의 DOM 을 외부에서도 사용하기 위해 컴포넌트에 직접 ref 전달 == DOM 에 ref 를 다는 것과 같음 */}
          <div ref={quillElement} />
      </QuillWrapper>
  </EditorBlock>
  
  
  );
};

export default Editor;

 

이렇게 썼는데, quill 이벤트 핸들러에 관한 내용은, 

요기에서 설명한대로에 기반하여 적고있움.

 

https://quilljs.com/docs/api/#event

 

API - Quill

API Content deleteText Deletes text from the editor, returning a Delta representing the change. Source may be "user", "api", or "silent". Calls where the source is "user" when the editor is disabled are ignored. Methods deleteText(index: Number, length: Nu

quilljs.com

출처 - https://quilljs.com/docs/api/#text-change

 

 

끌끌ㄹ 어렵구먼 어려워 내 소원이 있다면.... 장고나 리액트의 라이브러리를 자유롭게 내가 가져와서 쓸 수 있는 멋진 어른이 되는 것..그거시 나의 장래희망....ㅠ...

 

쨋든 이렇게 적어주면

이렇게 리덕스 스토어값에 잘 들어가는 것을 볼 수 있음!

 

 

이제는 태그박스 컨테이너 만들어주자...후...

 

src/containers/wirte/TagBoxContainer.js ( 새로 생성 )

import React from 'react';
import { useDispatch,useSelector } from 'react-redux';
import TagBox from '../../components/write/TagBox';
import { changeField } from '../../modules/write';



const TagBoxContainer = () => {
    const dispatch = useDispatch();
    const tags = useSelector(state => state.write.tags);
    const onChangeTags = nextTags =>{
        dispatch(
            changeField({
                key:'tags',
                value:nextTags,
            }),
        );
    };
    return (
        <TagBox onChangeTags={onChangeTags} tags={tags} />
    );
};

export default TagBoxContainer;

이제는 약간 흐름은 잡힌다..컨테이너 만들어줬으면, 페이지에서 컨테이너로 렌더링하는거 바꿔주고 받은 props TagBox 에서 처리해주기 음음.. 가자!

 

src/pages/WritePage

// create
import React from 'react';
import Responsive from '../components/common/Responsive';
// EditorContainer import
import EditorContainer from '../containers/write/EditorContainer';
import WriteActionButton from '../components/write/WriteActionButton';
import TagBoxContainer from '../containers/write/TagBoxContainer';

const WritePage = () => {
  return (
    <div>
      <Responsive>
        
        <EditorContainer />
           {/* 교체 */}
        <TagBoxContainer />
        <WriteActionButton />
      </Responsive>
    </div>
  );
};

export default WritePage;

 

src/compoentns/write/TagBox

const insertTag = useCallback(
    //   submit 했을 때 해당 함수 실행
    (tag) => {
      if (!tag) return; // 공백이면 추가하지 않음.
      if (localTags.includes(tag)) return; // 이미 존재하면 추가하지 않음.
    // //   state 에서는 배열, 객체를 바꿔줄 때 불변성 유지를 위해 spread 연산자를 사용해서 값을 업데이트 해야함.
    //   setLocalTags([...localTags, tag]);
    // },
    // [localTags],
    
    // onChangeTags 도 추가해서 컴포넌트 내부에서 상태기 바뀌면 리덕스 스토어에도 반영되고, 리덕스 스토어에 있는 값이 바뀌면 컴포넌트 내부의 상태도 바뀜.
    const nextTags = [ ...localTags, tag];
    setLocalTags(nextTags);
    onChangeTags(nextTags);
    },
    [localTags,onChangeTags]
  );
  
  
    const onRemove = useCallback(
    (tag) => {
      // 클릭한 해당 요소만 빼고 새로운 배열 반환하는 filter 함수
      const nextTags = localTags.filter((t) => t !== tag);
      setLocalTags(nextTags);
      onChangeTags(nextTags);
    },
    [localTags, onChangeTags],
  );
  // onSubmit 함수 밑에 적기
  // tags 값이 바뀔 때 
  useEffect(()=>{
    setLocalTags(tags);
  },[tags]);

 

전체로 바로 또 하면 이부분은 헷갈릴거같아서 수정된 부분을 중심으로 먼저 보여주고, 그 다음에 전체코드!

주석을 보면 그전에는  setLocalTags 만 적어서 컴포넌트 내부의 상태만 관리해줬다면, onChangeTags 도 같이 호출해서 이제는 스토어의 상태가 바뀌어도 TagBox 컴포넌트의 내부의 상태도 바뀌게 했다.

 

 

// useState,useEffect import
import React, { useCallback, useEffect, useState } from 'react';
// styled-components import
import styled from 'styled-components';
// palette import
import palette from '../../lib/styles/palette';

const TagBoxBlock = styled.div`
  width: 100%;
  border-top: 1px solid ${palette.gray[2]};
  padding-top: 2rem;

  h4 {
    color: ${palette.gray[8]};
    margin-top: 0;
    margin-bottom: 0.5rem;
  }
`;

const TagForm = styled.form`
  border-radius: 4px;
  /* 넘치는 form hidden */
  overflow: hidden;
  display: flex;
  width: 256px;
  /* style 초기화 ( 위에서 준 border-top style ) */
  border: 1px solid ${palette.gray[9]};
  /* input, button에 똑같은 style 주기 */
  input,
  button {
    outline: none;
    border: none;
    font-size: 1rem;
  }

  input {
    padding: 0.5rem;
    flex: 1;
    min-width: 0;
  }
  button {
    cursor: pointer;
    padding-right: 1rem;
    padding-left: 1rem;
    border: none;
    background: ${palette.gray[8]};
    color: white;
    font-weight: bold;
    &:hover {
      background: ${palette.gray[6]};
    }
  }
`;

const Tag = styled.div`
  margin-right: 0.5rem;
  color: ${palette.gray[6]};
  cursor: pointer;
  &:hover {
    opacity: 0.5;
  }
`;

const TagListBlock = styled.div`
/* 태그들을 가로로 정렬 */
  display: flex;
  margin-top: 0.5rem;
`;

// 렌더링을ㄹ 최적화하기 위해 TagItem ,TagList 로 두 가지 컴포넌트 분리. 만약 한 컴포넌트에서 직접 렌더링시  input 값이 바뀔 때 태그의 목록도 리렌더링됨. 또 태그 목록이 리렌더링 되면 태그하나하나 모두 리렌더링

// React.memo 사용으로 tag 값이(props 가 실제로 바뀔 때만) 바뀔 때만 리렌더링 되도록 처리
const TagItem = React.memo(({ tag, onRemove }) => (
  // tag 를 받아서 렌더링
  // 태그를 클릭할 때 onRemove 함수 실행
  <Tag onClick={() => onRemove(tag)}>#{tag}</Tag>
));

// 마찬가지로 React.memo 사용해서 tags 값이 바뀔 때만 리렌더링 되도록 처리
const TagList = React.memo(({ tags, onRemove }) => (
  <TagListBlock>
    {tags.map((tag) => (
      //  map 을 사용하여 컴포넌트로 변환할 때는 key 값 필요. tag 값을 key 값으로 설정
      // tag 데이터를 통째로 props 로 전달할 거임. 객체 통째로 전달해주는게 나중에 여러 종류의 값을 전달해야 하는 경우 최적화가 편함.
      // onRemove 함수 props
      <TagItem key={tag} tag={tag} onRemove={onRemove} />
    ))}
  </TagListBlock>
));

const TagBox = ({tags, onChangeTags }) => {
  // input, tags state
  const [input, setInput] = useState('');
  const [localTags, setLocalTags] = useState([]);

//   해당 2번째 인자로 받은 state 값들이 바뀔 때마다만 렌더링 해줘야 최적화기 떄문에 useCallback 사용


  const insertTag = useCallback(
    //   submit 했을 때 해당 함수 실행
    (tag) => {
      if (!tag) return; // 공백이면 추가하지 않음.
      if (localTags.includes(tag)) return; // 이미 존재하면 추가하지 않음.
    // //   state 에서는 배열, 객체를 바꿔줄 때 불변성 유지를 위해 spread 연산자를 사용해서 값을 업데이트 해야함.
    //   setLocalTags([...localTags, tag]);
    // },
    // [localTags],

    // onChangeTags 도 추가해서 컴포넌트 내부에서 상태기 바뀌면 리덕스 스토어에도 반영되고, 리덕스 스토어에 있는 값이 바뀌면 컴포넌트 내부의 상태도 바뀜.
    const nextTags = [ ...localTags, tag];
    setLocalTags(nextTags);
    onChangeTags(nextTags);
    },
    [localTags,onChangeTags]
  );

  const onRemove = useCallback(
    (tag) => {
        // 클릭한 해당 요소만 빼고 새로운 배열 반환하는 filter 함수
      setLocalTags(localTags.filter((t) => t !== tag));
    },
    [localTags],
  );

  const onChange = useCallback((e) => {
      //input onchange event 로 target value 를 상태값으로 업데이트
    setInput(e.target.value);
  }, []);

  const onSubmit = useCallback(
    (e) => {
      console.log(e);
      e.preventDefault();
    //  setInput 으로 업데이트된 값이 input 에 담겨져있을테니 얘를 앞뒤의 공백을 없애는 trim을 써서 tag 값 업데이트
      insertTag(input.trim()); 
      // input 초기화
      setInput('');
    },
    [input, insertTag],
  );

  // tags 값이 바뀔 때 
  useEffect(()=>{
    setLocalTags(tags);
  },[tags]);


  return (
    <TagBoxBlock>
      <h4>태그</h4>
      <TagForm onSubmit={onSubmit}>
        <input
          placeholder="태그를 입력하세요"
          value={input}
          onChange={onChange}
        />
        <button type="submit">글 추가</button>
      </TagForm>
      {/* 업데이트 된 state 값을 tags props로, 클릭했을 때 onRemove 함수 실행 */}
      <TagList tags={localTags} onRemove={onRemove} />
    </TagBoxBlock>
  );
};

export default TagBox;

 

전체 코드! 아구.. 헷갈려어어ㅠㅠㅠㅠ 이제 리덕스 스토어에 잘들어갈려남

 

 

실행 화면

 

잘들어가는거 확인! 또 클릭했을 때도 onRemove 로 잘 지워져야한다.

 

 


 

 

....음음...이제 마지막으로 API 연동...해야겠지 털썩털썩.... 젤어려워ㅠㅠㅠㅠ 사가가 아직 나한텐 넘 어렵다..후...쨋든 가자!

 

 

lib/api/posts.js ( 새로 생성 )

import client from "./client";


export const writePost = ({ title, body, tags}) =>
    client.post('/api/posts',{title,body, tags})

posts 파일을 새로 만들어서 post 관련 API 요청하는 함수 만들어주기.

 

 

이 함수를 호출하는 리덕스 액션과 사가 만들러가자...!

 

src/modules/write

import { createAction, handleActions } from 'redux-actions';
import createRequestSaga,{
    createRequestActionTypes,
} from '../lib/createRequestSaga';
 
import * as postsAPI from '../lib/api/posts';
import { takeLatest } from 'redux-saga/effects';

// 액션 타입 정의

const INITIALIZE = 'write/INITIALIZE';
const CHANGE_FIELD = 'write/CHANGE_FIELD';
// createRequestSaga 에서는 반복되는 부분을 함수화해서 정리해주기 위해서 createRequestActionTypes 사용해서 한번에 적음.
// 글쓰기 관련
const [ WRITE_POST, WRITE_POST_SUCCESS, WRITE_POST_FAIURE] = createRequestActionTypes('write/WRITE_POST')

// 액션 생성 함수
export const initialize = createAction(INITIALIZE);
export const changeField = createAction(CHANGE_FIELD,({ key, value })=>({
    key,
    value
}));

export const writePost = createAction(WRITE_POST, ({title,body, tags}) =>({
    title,
    body,
    tags,
}))

// saga 생성
const writePostSaga = createRequestSaga(WRITE_POST, postsAPI.writePost);

export function* writeSaga(){
    yield takeLatest(WRITE_POST,writePostSaga);
}
// 초기 상태 정의
const initialState = {
    title:'',
    body:'',
    tags:[]
};


// 리듀서 함수

const write = handleActions(
    {
        [INITIALIZE]: state => initialState, // initialState 를 넣으면 초기 상태로 바뀜
        [CHANGE_FIELD] : (state,{payload: { key,value }}) =>({
            ...state,
            [key] : value, // 특정 key 값 업데이트
        }),
        [WRITE_POST]: state =>({
            ...state,
            // post, postError 초기화
            post:null,
            postError:null
        }),
        // post success
        [WRITE_POST_SUCCESS] : ( state, {payload : post}) =>({
            ...state,
            post
        }) ,
        //post fail
        [WRITE_POST_FAIURE] : (state, {payload:postError}) =>({
            ...state,
            postError
        }) ,
    },
    initialState
)


export default write;

모듈에서 사가를 생성해서 API 처리를 하게 만든다음에는 !! 사가를 만들어줬으니 역시 루트 사가에 등록!

 

 

src/modules/index

 

으으 이제 호출준비를 했으니까 호출해서 구현할 작업을 버튼 컴포넌트 컨테이너 컴포넌트를 만들어서 적용만 해주면 된다! 

 

 

src/containers/write/WriteActionButtonsContainer.js ( 새로 생성 )

import React, { useEffect } from 'react';
import WriteActionButton from '../../components/write/WriteActionButton';
import { useSelector, useDispatch } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { writePost } from '../../modules/write';
import { write } from '../../../node_modules/ieee754/index';

const WriteActionButtonContainer = ({history}) => {
  const dispatch = useDispatch();

  const { title, body, tags, post, postError } = useSelector(({ write }) => ({
    title: write.title,
    body: write.body,
    tags: write.tags,
    post: write.post,
    postError: write.postError,
  }));
  //  컴포넌트에서 onClick 이벤트로 호출할 함수
  const onPublish = () => {
    dispatch(
      // 리덕스 스토어 안에 들어있는 값을 사용.
      writePost({
        title,
        body,
        tags,
      }),
    );
  };

  const onCancel = () =>{
    // history 객체 사용으로 뒤로 가기
      history.goBack()
  };

  useEffect(()=>{
    // post 작성이 성공하면 
      if(post){
          const {_id, user } = post;
          // _id, username 값을 참조해서 포스트를 읽을 수 있는 detail 경로를 만듬. 그리고 해당 경로로 이동
          history.push(`/@${user.username}/${_id}`);
      }

  if(postError){
      console.log(postError)
  }
},[history, post, postError]);

  return (
  <WriteActionButton onPublish={onPublish} onCancel={onCancel} />
  
  );
};
// 라우트가 아닌 컴포넌트에서 history 객체를 사용하기 위해서 컴포넌트를 withRouter 로 감싸줌
export default withRouter(WriteActionButtonContainer);

이제 컨테이너를 만들어줬으면 !??!?!

 

 

페이지에서 바꿔치기 time~

 

// create
import React from 'react';
import Responsive from '../components/common/Responsive';
// EditorContainer import
import EditorContainer from '../containers/write/EditorContainer';
import WriteActionButton from '../components/write/WriteActionButton';
import TagBoxContainer from '../containers/write/TagBoxContainer';
import WriteActionButtonContainer from '../containers/write/WriteActionButtonContainer';

const WritePage = () => {
  return (
    <div>
      <Responsive>
        
        <EditorContainer />
          
        <TagBoxContainer />
         {/* 교체 */}
        <WriteActionButtonContainer />
      </Responsive>
    </div>
  );
};

export default WritePage;

두근..실행 해볼까

 

실행 화면

 

호오....post 작성에 성공하면 username/post_id 값대로 이동이 잘 되어야 하고,

포스트 읽기 창이 뜨면서 리덕스 write.post 값을 확인했을 때 잘 들어가있으면 성공!!!! 꺆ㄲ!!!!!!!

또 포스트 작성 화면에서 취소를 누르면 바로 내가 그 전에 머물렀던 페이지로 까지 돌아가면 오늘은 정말 끝!!!!!

 

 


나는....왜 항상 갑자기 이렇게 줄줄 적을까...오늘은 한 포스팅에서 끝내겠다 했지만...뭐가 또 많아진 느낌...흑....흑..... 글이 길어지면 계속 또 까먹는데...후..... 하지만 블로그 때문에 어제오늘 ㄹㅇ 열시미 달렸다..코딩....블로그는 진챠 내가 공부하려고 시작했다가 나름 꾸준히 적고있는 첫번째 결과물이라 넘무..뿌듯... 근데 요즘 왤케 방문수가 ....많지....부담시러버....헣허허..... 끌끌 뭔가 회고글 적는 기점으로 살람이....많이 들어오고 있는 건 기분탓인가 ㅋㅋㅋㅋㅋ 껄껄 쨋든 달렸기 때문에 후회없는 오늘의 포스팅 끝!

 

댓글