인스타그램 클론코딩 11일차 - FE

박병준·2021년 8월 7일
1
post-thumbnail

#7.0 Header

login이나 signup 페이지에서는 헤더가 안보이게 한다. 로그인된 header에 user의 avatar를 담기 위해 user를 가져오는 hook을 만들어 사용한다.

client에서 http의 header에 token을 실어서 보낸다.

//apollo.js
const httpLink = createHttpLink({
    uri: "http://localhost:4000/graphql",
});

const authLink = setContext((_, { headers }) => {
    return {
        headers: {
            ...headers,
            token: localStorage.getItem(TOKEN),
        },
    };
});

export const client = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
});

BE에서 token이 안맞을경우 null을 반환하게끔 했기 때문에 token에 장난을 쳤다면 useEffect를 이용해서 null이면 Local storage에서 지워주도록 한다.

//hooks/useUser.js
import { gql, useQuery, useReactiveVar } from "@apollo/client";
import { useEffect } from "react";
import { isLoggedInVar, logUserOut } from "../../apollo";

const ME_QUERY = gql`
  query me {
    me {
      username
      avatar
    }
  }
`;

function useUser() {
    const hasToken = useReactiveVar(isLoggedInVar);
    const { data } = useQuery(ME_QUERY, {
        skip: !hasToken,
    });

    useEffect(() => {
        if (data?.me === null) {
            logUserOut();
        }
    }, [data])
    return;
}
export default useUser;

#7.1 Photo Component

Photo component를 만든후 query를 통해 값을 가져와서 map함수를 통해 Feed를 보여준다.

//Home.js
const FEED_QUERY = gql`
    query seeFeed {
    seeFeed {
      id
      user {
        username
        avatar
      }
      file
      caption
      likes
      comments
      createdAt
      isMine
      isLiked
    }
  }
`;

function Home() {
    const { data } = useQuery(FEED_QUERY);
    return (
        <div>
            {data?.seeFeed?.map((photo) => <Photo key={photo.id} {...photo} />)}
        </div>
    );
}

export default Home;

#7.2 Liking

좋아요를 느르면 실시간으로 DB값이 바뀌면서 client가 보는 프론트도 변경되어야한다. 이 때 방법으로 세가지가 있다.

첫번 째는 Refetching Quries이다. 전체query를 DB로부터 다시 가져와 refetch하는 방법이다. 모든 query를 refetch하기 때문에 실시간에서 좋은 방법은 아니다.

// Photo.js
function Photo({ id, user, file, isLiked, likes }) {
    const [toggleLikeMutation, { loading }] = useMutation(TOGGLE_LIKE_MUTAITON, {
        variables: {
            id,
        },
        refetchQueries: [{query: FEED_QUERY}]
    });

두번째 방법은 Fragment를 이용하는 것이다. useMutation의 update를 사용하여 writeFragment로 cache에 있는 값을 빠르게 사용한다. 즉 DB는 mutation이 프론트는 캐시 값으로인 것이다.

// Photo.js
function Photo({ id, user, file, isLiked, likes }) {
    const updateToggleLike = (cache, result) => {
        const { data: { toggleLike: { ok } } } = result;
        if (ok) {
            cache.writeFragment({
                id: `Photo:${id}`,
                fragment: gql` //여기서 any는 아무 단어나 해도된다.
                    fragment any on Photo { 
                        isLiked
                        likes
                    }
                `,
                data: {//만약 isLiked나 likes가 인자로 받아올 수 없을 때
                    isLiked: !isLiked,//readFragment로 받아올 수 있다.
                    likes: isLiked ? likes - 1 : likes + 1,
                }
            })
        }
    };

    const [toggleLikeMutation] = useMutation(TOGGLE_LIKE_MUTAITON, {
        variables: {
            id,
        },
        update: updateToggleLike,
    });

세번째 방법은 Cache Modify이다. 두번째와 비슷하지만 두번째보다 짧게 코드를 작성할 수 있다.

// Photo.js
function Photo({ id, user, file, isLiked, likes, caption, totalComments, comments }) {
    const updateToggleLike = (cache, result) => {
        const { data: { toggleLike: { ok } } } = result;
        if (ok) {
            const photoId = `Photo:${id}`;
            cache.modify({
                id: photoId,
                fields: {//prev는 이전 값
                    isLiked(prev) {
                        return !prev;
                    },
                    likes(prev) {
                        if (isLiked) {
                            return prev - 1;
                        }
                        else {
                            return prev + 1;
                        }
                    }
                }
            })
        }
    };

#7.3 Parsing Hashtags

React에서는 기본적으로 Html을 사용할 수 없다. 하지만 hashtag를 parsing할 때 html태그인 mark태그를 사용하고싶어dangerouslySetInnerHTML를 사용하게되면 나중에 사용자가 html태글를 댓글에 넣었을 떄 그대로 적용될 수 있다.

//Commet.js
function Comment({ author, payload }) {
    return (
        <CommentContainer>
            <FatText>{author}</FatText>
            <CommentCaption dangerouslySetInnerHTML={{
                __html: payload?.replace(/#[ㄱ-ㅎ|ㅏ-ㅣ|가-힣|\w]+/g, "<mark>$&</mark>")
            }} />
        </CommentContainer>
    );
}

위 문제를 해결하기 위해 sanitize-html를 이용한다.
npm i sanitize-html

sanitize-html는 내가 사용하고 싶은 html로만 걸러준다. 아래를 보면 mark 태그만 허용한다.

//Comment.js
function Comment({ author, payload }) {
    const cleanPayload = sanitizeHTML(
        payload?.replace(/#[ㄱ-ㅎ|ㅏ-ㅣ|가-힣|\w]+/g, "<mark>$&</mark>"),
        {
            allowedTags: ["mark"],
        }
    )
    return (
        <CommentContainer>
            <FatText>{author}</FatText>
            <CommentCaption dangerouslySetInnerHTML={{
                __html: cleanPayload,
            }} />
        </CommentContainer>
    );
}

하지만 여기서도 문제가 있다. 우리는 hashtag를 통해 검색 창으로 넘어가야한다. 다른 페이지로 가려면 a태그를 허용해야하는데 comment에 사용자가 a태그를 사용하면 위험하다.

따라서 한 단어씩 잘라서 Fragment를 이용해 넣는다. 이 때 Fragment는 그냥 쓸 경우 key를 안가지므로 React.Fragment로 사용하여야 한다.

  • Fragment = <> </>
// Comment.js
function Comment({ author, payload }) {
    return (
        <CommentContainer>
            <FatText>{author}</FatText>
            <CommentCaption>
                {payload.split(" ").map((word, index) =>
                    /#[ㄱ-ㅎ|ㅏ-ㅣ|가-힣|\w]+/g.test(word) ? (
                        <React.Fragment key={index}>
                            <Link to={`/hashtags/${word}`}>{word}</Link>{" "}
                        </React.Fragment>
                    ) : (
                        <React.Fragment key={index}>{word} </React.Fragment>
                    )
                )}
            </CommentCaption>
        </CommentContainer>
    );
}

#7.4 Comments

위의 방식대로 data를 create할 때도 cache를 이용해서 DB따로 프론트따로 변화시킬 수 있다. 하지만 위에 방식에서 create는 DB는 추가하지만 캐시는 추가하지 않는다. 그렇게 되면 Delete를 할 때 캐시에서 새로만든 comment는 delete할 수 없다. 따라서 cache에서 comment객체를 만들고 그것을 Photo에 붙여준다.

//Comments.js
const createCommentUpdate = (cache, result) => {
        const { payload } = getValues();
        setValue("payload", "");
        const {
            data: {
                createComment: { ok, id },
            }
        } = result;
        if (ok && userData?.me) {
            const newComment = {
            	__typename: "Comment",//캐시는 __typename필수
                id,
                user: { ...userData.me },
                payload,
                isMine: true,
                createdAt: Date.now() + "", //createdAt은 string이기 때문
            };
            const newCacheComment = cache.writeFragment({
              	//새로 만드는 것이기 때문에 id는 필요없다.
                fragment: gql`
                    fragment any on Comment { 
                        id
                        user{
                            username
                            avatar
                        }
                        payload
                        isMine
                        createdAt
                    }
                `,
                data: newComment,
            })
            cache.modify({
                id: `Photo:${photoId}`,
                fields: {
                    totalComments(prev) {
                        return prev + 1;
                    },
                    comments(prev) { //배열 뒤에 넣어주는 방법
                        return [...prev, newCacheComment];
                    }
                }
            });
        }
    };

delete는 cache.evict를 사용하면 쉽게 cache에서 delete된다.

const deleteCommentUpdate = (cache, result) => {
    const { data: { deleteComment: { ok } } } = result;
    if (ok) {
        cache.evict({ id: `Comment:${id}` });
        cache.modify({
            id: `Photo:${photoId}`,
            fields: {
                totalComments(prev) {
                    return prev - 1;
                },
            },
        });
    }
};

const [deleteComment] = useMutation(DELETE_COMMENT_MUTATION, {
    variables: {
        id,
    },
    update: deleteCommentUpdate,
});
profile
뿌셔뿌셔

0개의 댓글