본문 바로가기
React

2021.09.28 React Update,Delete

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

2021.09.28 React Update,Delete_정리노트

 

드디어 마지막장...! 완독이 얼마 남지 않았으 마지막까지 빠이링~!~!

 

 

포스트 해당 작성자에게 보여줄 수정,삭제 버튼 UI 부터 만들기! 기존에 Button 과 달라서 이번에는 재사용하지 않고 따로 버튼 만들어서 사용할 예정!

 

 

components/post/PostActionButton.js (새로 생성)

import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';

const PostActionButtonBlock = styled.div`
  display: flex;
  justify-content: flex-end;
  margin-bottom: 2rem;
  margin-top: -1.5rem;
`;

const ActionButton = styled.button`
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  color: ${palette.gray[6]};
  border: none;
  outline: none;
  font-size: 0.875rem;
  cursor: pointer;
  &:hover {
    background: ${palette.gray[1]};
    color: ${palette.violet[7]};
  }
  & + & {
    margin-left: 0.25rem;
  }
`;

const PostActionButton = () => {
  return (
    <PostActionButtonBlock>
      <ActionButton>수정</ActionButton>
      <ActionButton>삭제</ActionButton>
    </PostActionButtonBlock>
  );
};

export default PostActionButton;

 

이렇게 만들어주고 

 

 

얘를 여기서 밑에 적어줄려고했는데, PostViewer 에서 직접 PostActionButton 를 렌더링하게 되면, onEdit,onRemove 같은 props 를 전달할 때 무조건 PostViewer 를 거쳐서 전달해야한다. 

 

// PostViewerContainer 에서 렌더링할 때
<PostViewer onEdit={onEdit} onRemove={onRemove} />

// PostViewer 에서 렌더링할 때,
<PostActionButton onEdit={onEdit} onRemove={onRemove} />

 

요롷게! 정작 PostViewer 자체 내부에서는 사용하지 않지만, 이 안에 내부의 컴포넌트인 PostActionButton에서 이를 사용하기 때문!

이렇게 되면 안에서 받아오는 props 가 많아진다면 관리하기 까다로울 수 있어서 책에서는 그냥 새로운 컨테이너 컴포넌트 없이 직접 PostViewerContainer 에서 PostActionButton 컴포넌트를 직접 JSX 형태 props 로 받아오는 방법을 사용했다. 

 

 

그래서 우리는 PostViewerContainer 컴포넌트에 

경로 캡쳐본 상단 참고!

 

이렇게 컴포넌트 자체 JSX 를 props 로 설정해준 다음에,

 

src/components/post/PostViewer

 

PostViewer 에서 읏챠하고 받아와서 

이렇게 PostHead 밑에 적어주었다. 그러면 

 

 

실행 화면

이렇게 수정 삭제 버튼이 디테일 페이지에 잘 뜬다!! 얼 ㅋ 신기하댜 이제 수정 기능을 구현하고 해당 글을 작성한 유저한테만 이 버튼을 볼 수 있게 차례대로 구현하러 ㄲ!

 

 

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')
// 현재 보고 있는 포스트의 정보의 상태를 가져오는 액션
const SET_ORIGINAL_POST = 'write/SET_ORIGINAL_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,
}))

export const setOriginalPost = createAction(SET_ORIGINAL_POST, post => post)

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

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


// 리듀서 함수

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
        }) ,
        [SET_ORIGINAL_POST]: (state, {payload: post} )=> ({
            ...state,
            title:post.title,
            body:post.body,
            tags: post.tags,
            originalPostId:post._id,
        })
    },
    initialState
)


export default write;

 

write 모듈에 현재 보고 있는 포스트의 상태를 가져오는 액션을 만들고, setOriginalPost 이라는 액션 생성함수와, 초기 상태에 postid 값을 null 로 세팅, 그리고 리듀서도 작성해주었따!

 

그러고 나서 컨테이너 컴포넌트도 같이 수정해주장.

 

src/containers/post/PostViewerContainer

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router';
import { readPost, unloadPost } from '../../modules/post';
import PostViewer from '../../components/post/PostViewer';
// PostActionButton import
import PostActionButton from '../../components/post/PostActionButton';
import { setOriginalPost } from '../../modules/write';

const PostViewerContainer = ({ match, history }) => {
  //  match 객체를 사용하면 해당 객체의 params 값을 참조할 수 있음
  // 요 안에는 현재 컴포넌트가 어떤 경로 규칙에 의해 보이는지에 대한 정보가 들어있음
  const { postId } = match.params;
  const dispatch = useDispatch();
  const { post, error, loading, user } = useSelector(({ post, loading ,user}) => ({
    post: post.post,
    error: post.error,
    loading: loading['post/READ_POST'],
    user:user.user
  }));

  useEffect(() => {
    dispatch(readPost(postId));
    // 언마운트 될 때 리덕스에서 포트스 데이터 없애기
    return () => {
      dispatch(unloadPost());
    };
  }, [dispatch, postId]);

  const onEdit = () => {
    dispatch(setOriginalPost(post));
    history.push('/write');
  }

  const ownPost = (user && user._id) === (post && post.user._id);


  return <PostViewer post={post} loading={loading} error={error} actionButtons={ownPost && <PostActionButton onEdit={onEdit} />} />;
};

// url 파라미터로 받아온 id 값을 조회하는 match 객체에 접근하기 위해 withRouter 사용
export default withRouter(PostViewerContainer) ;

history 객체를 사용해서 onEdit 함수를 구현해서 PostActionButtons 에 전달해주고, post 의 user 와 현재 user 의 id 값을 비교해서 같으면 해당 PostActionButtons 가 나타나도록 구현했다. 이제 props 값을 전달했으니까 PostActionButtons 에서 받아주자!

 

src/components/post/PostActionButton

 

점점 기능이 추가 되는 식이라 헷갈릴 거 같아서 이번 포스트에서는 캡쳐본이 많을듯 ?? 쨋든 이렇게 클릭했을 때 해당 함수를 실행하도록 만들어주고 수정버튼을 눌러보면,

 

 

아직 내용까지는 안불러오는 것을 알 수 있음. quill 을 썼기 때문에 아직 내용은 못불러 오고 있는 것!!  Editor 로 갑씨당. 

 

src/components/wirte/Editor

이 에디터 컴포넌트에서 받아오는 body 의 값은 quill 에디터에서 내용을 입력할 때마다 변경되는데, body 가 변경될 때 마다 해당 useEffect 에 등록된 함수가 호출된다. 하지만 컴포넌트가 화면에 마운트 될 때 한번만 이 작업이 수행되게 해야되겠지 ?? 수정하려고 다른 글을 입력할 때도 기존 내용이 나오면 안되니까 ㅇㅇ 그래서 원래는 빈 배열을 넣어주면 초기 렌더링 마운트 후 한번만 수행되는데, eslint 의 규칙의 useEffect 에서는 내부에서 사용되는 외부값을 모두 2번째 파라미터 배열에 넣는 것을 권장하기 때문에, 이런식으로 처리해준 것.

 

 

만약 이게 이해가 안된다면,

 useEffect(()=>{
        quillInstance.current.root.innerHTML = body;
         },[]); /* eslint-disable-line */

 

이렇게 적는 방법도 있다! 음음 여러가지 방법이 있넹

이 부분 잘 모르겠어서 찾아보니까 

https://www.zerocho.com/category/React/post/5f9a7c8807be1d0004347311

 

(React) React Hooks! useRef편(React 17버전)

이번 시간에는 useRef hook을 배워봅시다. 여기까지가 자주 쓰이는 훅인 것 같습니다. useContext, useReducer, useImperativeHandle, useLayoutEffect도 리액트의 기본 훅이지만 상대적으로 사용 빈도가 떨어집니다.

www.zerocho.com

요기 참고해서 주석으로 정리했음! 좋당좋당

 

이제 실행하면,

 

이렇게 내용까지 잘 뜨고 스토어에도 해당 값들이 잘 들어가 있당

 

여기서 originalPostId 값이 주어졌으니까 이제는 포스트 작성 API 대신에 수정 API 를 사용해주면 되시겠다~~~!!

 

 

src/lib/api/posts

 

 

update 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')
// 현재 보고 있는 포스트의 정보의 상태를 가져오는 액션
const SET_ORIGINAL_POST = 'write/SET_ORIGINAL_POST';

//post 수정 관련 액션타입 정의
const [ UPDATE_POST, UPDATE_POST_SUCCESS,UPDATE_POST_FAILURE] =createRequestActionTypes('write/UPDATE_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,
}))

export const setOriginalPost = createAction(SET_ORIGINAL_POST, post => post)

export const updatePost = createAction(
    UPDATE_POST,
    ({ id, title, body, tags }) =>({
        id,
        title,
        body,
        tags,
    })
)

// saga 생성
const writePostSaga = createRequestSaga(WRITE_POST, postsAPI.writePost);
const updatePostSaga = createRequestSaga(UPDATE_POST, postsAPI.updatePost);
export function* writeSaga(){
    yield takeLatest(WRITE_POST,writePostSaga);
    yield takeLatest(UPDATE_POST,updatePostSaga);
}
// 초기 상태 정의
const initialState = {
    title:'',
    body:'',
    tags:[],
    originalPostId: null,
};


// 리듀서 함수

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
        }) ,
        [SET_ORIGINAL_POST]: (state, {payload: post} )=> ({
            ...state,
            title:post.title,
            body:post.body,
            tags: post.tags,
            originalPostId:post._id,
        }),
        [UPDATE_POST_SUCCESS] : (state,{payload:post}) =>({
            ...state,
            post
        }),
        [UPDATE_POST_FAILURE] : (state, {payload :postError}) =>({
            ...state,
            postError
        })
    },
    initialState
)


export default write;

 

이제 컨테이너 두개 수정정정

 

src/containers/write/WriteActionButtonContainer

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,updatePost } from '../../modules/write';
import { write } from '../../../node_modules/ieee754/index';

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

  const { title, body, tags, post, postError,originalPostId } = useSelector(({ write }) => ({
    title: write.title,
    body: write.body,
    tags: write.tags,
    post: write.post,
    postError: write.postError,
    originalPostId : write.originalPostId,
  }));
  //  컴포넌트에서 onClick 이벤트로 호출할 함수
  const onPublish = () => {
    // postid 가 있으면 업데이트 액션 생성 함수 실행
    if ( originalPostId ){
      dispatch(updatePost({ title, body,tags, id: originalPostId}));
      return;
    }
    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 (
    // ! 는 하나쓰면 한번 부정, !! 는 한번 더 즉, 이중 부정. 그래서 해당 postid 값이 있냐에 따라서 버튼의 텍스트 값 바꿀 것임
    // 얘를 왜 쓰냐, 불리언으로 형변환을 위해 씀. 1 을 true 로, 0을 false 로 와 같은..!
  <WriteActionButton onPublish={onPublish} onCancel={onCancel} isEdit={!!originalPostId} />
  
  );
};
// 라우트가 아닌 컴포넌트에서 history 객체를 사용하기 위해서 컴포넌트를 withRouter 로 감싸줌
export default withRouter(WriteActionButtonContainer);

여기서 postid 값에 따라 update 액션 생성 함수를 실행 하는 if 문과 idEdit props 를 넘겨주었움.

 

props 넘겨줬으니까 WriteActionButton 에서 받아야겠디

 

src/componnents/write/WriteActionButton

 

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,isEdit }) => {
  return (
    <WriteActionButtonBlock>
        {/* click event props settings */}
      <StyledButton onClick={onPublish}>포스트 {isEdit ? '수정' : '등록'} </StyledButton>
       {/* button color props,click event props settings */}
      <StyledButton gray onClick={onCancel}>취소</StyledButton>
    </WriteActionButtonBlock>
  );
};

export default WriteActionButton;

 

두근 이제 실행해보까나

 

 

얼 수정이라고 문구도 잘 나타나고 업데이트하면

 

잘 반영되는 모습을 볼 수 있다!!!

 


 

 

 

 

굿 이제 마지막으로 삭제가 남았다!!! 삭제할 때는 버튼을 눌러서 바로 삭제가 아니라 사용자에게 확인을 한번 더 요청하고 나서 삭제를 하게 해서 실수로 삭제를 하지 않게 방지하기 까지 해보자!

 

그러면 확인 모달창 만들어야하니까 모달창 UI 부터 ㄲ!

 

src/components/common/AskModal.js ( 새로 생성 )

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

const FullScreen = styled.div`
  /* modal background */
  position: fixed;
  z-index: 30;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.25);
  display: flex;
  justify-content: center;
  align-items: center;
`;

const AskModalBlock = styled.div`
  /* modal content */
  width: 320px;
  background: white;
  padding: 1.5rem;
  border-radius: 4px;
  box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.125);
  h2 {
    margin-top: 0;
    margin-bottom: 1rem;
  }
  p {
    margin-bottom: 3rem;
  }
  .buttons {
    display: flex;
    justify-content: flex-end;
  }
`;

const StyledButton = styled(Button)`
  height: 2rem;
  & + & {
    margin-left: 0.75rem;
  }
`;

const AskModal = ({
  visible,
  title,
  description,
  confirmText = '확인',
  cancelText = '취소',
  onConfirm,
  onCancel,
}) => {
    // true 면 false  false 면 true로 반환. 토글 같은 기능
  if (!visible) return null;
  return (
    <FullScreen onClick={onCancel}>
      <AskModalBlock>
        <h2>{title}</h2>
        <p>{description}</p>
        <div className="buttons">
          <StyledButton gray onClick={onCancel}>{cancelText}</StyledButton>
          <StyledButton onClick={onConfirm}>
            {confirmText}
          </StyledButton>
        </div>
      </AskModalBlock>
    </FullScreen>
  );
};

export default AskModal;

 

이렇게 만들어주고 여기서는 포스트 읽기 페이지에서만 쓰지만 재사용성 고려해서 common 디렉토리에서 만들어주었움! 그리고 책에서는 취소버튼을 누르면 모달이 사라지는데 나는 거기에 더해서 까만 배경을 눌러도 모달이 사라지게 추가해주었움

 

이제 UI를 기반으로한 삭제 모달을 post 디렉토리에 만들겠움!

 

 

src/components/post/AskRemoveModal.js ( 새로 생성 )

 

import React from 'react';
import AskModal from '../common/AskModal';


const AskRemoveModal = ({visible, onConfirm, onCancel }) => {
    return (
        <AskModal
            visible={visible}
            title="포스트 삭제"
            description="포스트를 정말 삭제하시겠습니까?"
            confirmText="삭제"
            onConfirm={onConfirm}
            onCancel={onCancel}
        />
            
       
    );
};

export default AskRemoveModal;

 

오랜만에 props 를 텍스트로 전달하니까 뭔가 재밌닿ㅎㅎㅎㅎ 쉬워섷ㅎㅎㅎ .... 후 진정하거 머 이렇게 굳이 모달을 2개 나눠줄 필요는 이 프로젝트에선 하나만 쓰기 때문에 상관은 없지만, 모달 갯수가 여러개로 늘어나면 이렇게 분리해서 재사용하면 편할듯!

 

 

이제 만들었으니까 삭제버튼을 누르면 모달이 뜨면서 삭제가 되는 것까지 해봅시당!

 

src/lib/api/posts

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

// post read id 값 가져오기 위해서 backtic 사용
export const readPost = (id) => client.get(`/api/posts/${id}`);

// post list api with qs
export const listPosts = ({ page, username, tag }) => {
  const queryString = qs.stringify({
    page,
    username,
    tag,
  });
  //   예시 >>> '/api/posts?username=tester&page=2'
  return client.get(`/api/posts?${queryString}`);
};

// update api
export const updatePost = ({ id, title, body, tags }) =>
  client.patch(`/api/posts/${id}`, { title, body, tags });

//delete api
export const removePost = id => client.delete(`/api/posts/${id}`);

와 이제 마지막 delete API 까지 해준 모습 !!!  삭제같은 경우는 API 를 요청한 후 따로 보여주어야 할 결과가 없어서 리덕스 액션과 사가를 만들 필요없이 바로 API 를 사용!

 

src/containers/post/PostViewerContainer

 

이렇게 onRemove 함수를 넘겨줬으면 마지막으로 삭제버튼에서 받아주기만 하면 완성!

 

 

 

src/components/post/PostActionButton

import React, { useState, useCallback } from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import AskRemoveModal from './AskRemoveModal';
const PostActionButtonBlock = styled.div`
  display: flex;
  justify-content: flex-end;
  margin-bottom: 2rem;
  margin-top: -1.5rem;
`;

const ActionButton = styled.button`
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  color: ${palette.gray[6]};
  border: none;
  outline: none;
  font-size: 0.875rem;
  cursor: pointer;
  &:hover {
    background: ${palette.gray[1]};
    color: ${palette.violet[7]};
  }
  & + & {
    margin-left: 0.25rem;
  }
`;

const PostActionButton = ({ onEdit, onRemove }) => {
  const [modal, setModal] = useState(false);
// 삭제 버튼을 누르면 모달이 보임
  const onRemoveClick = () => {
    setModal(true);
  };
// 취소 버튼을 누르면 모달이 안보임
  const onCancel = () => {
    setModal(false);
  };
// 모달에서 확인을 누르면 삭제가 되면서 모달이 안보임
  const onConfirm = () => {
    setModal(false);
    // onRemove 함수 호출
    onRemove();
  };
  return (
    <>
      <PostActionButtonBlock>
        <ActionButton onClick={onEdit}>수정</ActionButton>
        <ActionButton onClick={onRemoveClick}>삭제</ActionButton>
      </PostActionButtonBlock>
      <AskRemoveModal
        visible={modal}
        onConfirm={onConfirm}
        onCancel={onCancel}
      />
    </>
  );
};

export default PostActionButton;

 

 

갸악 완성!!!! 실행해보자

 

 

실행 화면

 

여기서 취소랑 뒤의 까만 배경 누르면 visible 값이 false 가 되면서 모달창이 안보여야함.

 

 

 

그리고 삭제버튼을 누르면 post 가 지워지면서 홈화면으로 이동하면!!! 끝!!!!!

 


와 짝짝자가자짜ㅏ짝 길고 길었던 CRUD 끝!!!! 이제 정말 마지막으로 meta 태그만 바꾸고 끝낼게 ㅋㅅㅋㅅㅋㅅ

 

 

요부분 말임 얘를 우리가 원하는 이름으로 바꿔주고 각 페이지에서 원하는대로 해서 몇개 바꿔보겠움!

 

react-helmet-async 라는 라이브러리로 메타태그 달아줄거임.

$ yarn add react-helmet-async

 

그리고 index 파일에서 APP 컴포넌트를 해당 HelmetProvider 로 감싼다!

 

src/index

 

 

이렇게 해주고 메타태그를 설정하고 싶은 곳에 Helmet 컴포넌트만 써주면 끝~!~!

 

 

src/App

src/pages/WritePage

 

이 컴포넌트는 디렉토리가 더 깊숙한 곳에 있는 Helmet 이 우선순위이다. 그래서 App 에서 최상위로 적용시켰다가 더 깊숙한 곳에 있는 WritePage 에 이 컴포넌트를 적용하면

 

이렇게 WritePage 의 메타태그가 적용이 된다. 

 

 

src/components/post/PostViewer

 

 

그리고 이렇게 title 값을 가져와서 써주어도 잘 적용이 된다!

 

 


아 길고 길었던 프로젝트 드디어 끝...!!!!!!! ㅠㅠㅠ 책 완독했다 했어!!!!! 처음 책 완독해서 되게 설레는구만 홀홀 그리고 뭐하나 쉬운게 없다...^^ ㅋㅋㅋㅋㅋ 역시 CRUD 가 정말 기본이지만 얘를 이용해서 블로그와 커뮤니티 만드는 것등등 ㄹㅇ 할게 아직 산이다 산이야 후,,,,,,, 따로 완독은 후기쓸 수 있으면 쓰고 오늘 기분 좋게 여기서 끝! 다덜 이까지 따라온 사람 있다면 칭찬해!!!! 수고해써!!!!!!!

댓글