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;
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;
좋아요를 느르면 실시간으로 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;
}
}
}
})
}
};
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로 사용하여야 한다.
// 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>
);
}
위의 방식대로 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,
});