User module(3)
🍔 핵심 내용
🥑 follow기능 / unfollow 기능 추가
typeDefs와 resolvers를 추가해보자.
🥑 seeProfile에서의 include 사용법 (following, followers 값 보기)
following나 followers값이 엄청 나게 많으면 query시 db에 많은 부하를 준다. 따라서 디폴트 값으로는 해당 값을 호출 할 수 없다. 이 때 include를 사용하여 강제로 호출 할 수 있다.
🍔 코드 리뷰
🥑 followUser.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type FollowUserResult {
ok: Boolean!
error: String
}
type Mutation {
followUser(userName:String): FollowUserResult
}
`
🥑 followUser.typeDefs.js
import client from "../../client";
import { protectedResolver } from "../users.utils";
export default {
Mutation: {
followUser: protectedResolver(async (_, { userName }, { loggedInUser }) => {
const ok = await client.user.findUnique({ where: { userName } })
if (!ok) {
return {
ok: false,
error: "없는 계정 입니다."
}
}
await client.user.update({
where: {
id: loggedInUser.id
},
data: {
followings: {
connect: {
userName
}
}
}
})
return {
ok: true,
}
})
}
}
다른 큰 특이사항은 없고, 로긴한 id의 데이터에 값을 수정해 주어야 하기 때문에 context값인 loggedInUser를 넣어 주었다. 그리고, update 부분에 followings 부분에 connect 값을 넣어준다.
🥑 unfollowUser.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type UnfollowUserResult {
ok:Boolean!
error:String
}
type Mutation {
unfollowUser(userName:String!): UnfollowUserResult
}
`
🥑 unfollowUser.resolvers.js
import client from "../../client";
import { protectedResolver } from "../users.utils";
export default {
Mutation: {
unfollowUser: protectedResolver(async(_, {userName}, {loggedInUser})=> {
const ok = client.user.findUnique({
where: {userName},
})
if(!ok){
return {
ok:false,
error: "언팔 할 수 없습니다."
}
}
await client.user.update({
where: {
id: loggedInUser.id
},
data: {
followings:{
disconnect:{
userName
}
}
}
})
return {
ok:true,
}
})
}
}
follow 로직과 거의 유사하다
🥑 users.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type User {
id: String!
firstName: String!
lastName: String
userName: String!
email: String!
createdAt: String!
updatedAt: String!
bio: String
avatar: String
followers: [User] //추가
followings: [User] //추가
}
`;
먼저, seeProfle에서 user값을 불러와야 하기 때문에, followers와 following를 추가 해주자.
🥑 seeProfile.resolvers.js
import client from "../../client";
export default {
Query: {
seeProfile: (_, { userName }) => client.user.findUnique({
where: {
userName,
},
include: {
followings: true,
followers: true
} //db에 많은 부하를 준다. 일단 연습용으로 true로 두겠다.
})
},
};
include값이 없었다면, following와 followers 값이 null로 나오지만 위와 같이 include값을 강제로 넣어주면 해당 값을 확인 할 수 있다. (부하가 안걸릴거 같으면 open해도 될듯)
🍔 핵심 내용
🥑 follower수 확인, page 구성(pagination 이용하여, 확인해보기)
public 하게, 특정 user가 가지고 있는 follower를 확인해 볼 수 있는 Query를 만들어보자
🍔 코드 리뷰
🥑 seeFollowers.typeDefs.js
import { gql } from "apollo-server-core";
export default gql `
type SeeFollowersResult{
ok: Boolean!
error : String
followers: [User]
totalPages: Int
}
type Query {
seeFollowers(userName: String!, page: Int!): SeeFollowersResult!
}
`
followers에 User 배열이 들어갔다. [ {id:xx, ~~ }, {id:xx, ~~} ]
🥑 seeFollowers.resolvers.js
import client from "../../client";
export default {
Query: {
seeFollowers: async (_, { userName, page }) => {
const ok = await client.user.findUnique({
where: { userName },
select: { id: true },
});
if (!ok) {
return {
ok: false,
error: "유저를 찾을 수 없습니다.",
};
}
const followers = await client.user
.findUnique({ where: { userName } })
.followers({
take: 5,
skip: (page - 1) * 5,
});
const totalPages = await client.user.count({
where: { followings: { some: { userName } } },
});
return {
ok: true,
followers,
totalPages: Math.ceil(totalPages / 5),
};
},
},
};
const ok ~~ 이부분은, select를 통해 id 값만 가져올 수 있기 때문에 최적화 하는데 도움이 된다. (꿀팁)
take와 skip의 뜻은 take는 5개의 값을 가지고 온다는 거고, skip은 말그대로 특정 수를 스킵 한다는 거다.
take와 skip을 이용해 1page에 5개의 값만 가지고 올 수 있게 해두었다. 총 page수를 끌어오는 로직은 totalPages를 보면된다. some은 관계된 모든 값을 다 가지고 온다는 것임. 즉, user 모델에서 following값이 userName을 가지고 있는 모든 값을 가지고 오게 되면 userName의 followers 수를 파악할 수 있게 된다. (count는 수만 보는 함수임)
offset pagination 말고 cursor based pagination도 있는데, 먼저 offset pagination을 해보고 나서 버벅이거나 부하가 많이 된다면 cursor based pagination을 시도해본다. (무제한 스크롤 때 좋음) 이 방법은 다음 내용에..!
🍔 핵심 내용
🥑 cursor based pagination의 이해
대부분의 경우 offset pagination로 해도 상관이 없지만, 무제한 스크롤이 필요한 경우 이를 이용하면 좋다.
이를 이용하여, 이번에는 followings 수를 확인해보는 기능을 만들어보자.
cursor는 특정 마지막 값이후로 얼만큼의 데이터를 더 가지고 오는지를 볼 수 있다.
🍔 코드 리뷰
🥑 seeFollowings.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type seeFollowingsResult {
ok: Boolean!
error: String
followings: [User]
}
type Query {
seeFollowings(userName: String!, lastId: Int): seeFollowingsResult!
}
`;
lastId 부분에 마지막 값이 들어 간다. (필수 항목 아님)
🥑 seeFollowings.resolvers.js
import client from "../../client";
export default {
Query: {
seeFollowings: async (_, { userName, lastId }) => {
const ok = await client.user.findUnique({
where: { userName },
select: { id: true },
});
if (!ok) {
return {
ok: false,
error: "유저를 찾을 수 없습니다.",
};
}
const followings = await client.user
.findUnique({ where: { userName } })
.followings({
take: 5,
skip: lastId ? 1 : 0,
...(lastId && { cursor: { id: lastId } }),
});
return {
ok: true,
followings,
};
},
},
};
lastId의 값이 필수 값이 아니기 때문에, lastId ? 1 : 0 즉 lastId 값이 있으면 기본 skip값은 1 없으면 0 (첫 값부터 시작하겠다는 뜻)을 넣어주고, lastId 값이 있을 경우 cursor의 값을 넣기 위해 ...(lastId ~~~) 이 부분을 넣어준다.
나머지 로직은 seeFollowers 와 유사하다.
🍔 핵심 내용
🥑 Computed Fields의 이해
이번 내용에서는, resolvers의 인자 중 하나인 root를 이해할 수 있다. 이를 통해, DB 에 없는 값들을 화면에 뿌려줄 수 있다. (예를 들면, 팔로워 혹은 팔로잉하고 있는 전체 수)
팔로워 혹은 팔로잉 전체수를 파악 하기 위해 먼저 User 타입부분에 값을 추가해주자
🍔 코드 리뷰
🥑 users.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type User {
id: String!
firstName: String!
lastName: String
userName: String!
email: String!
createdAt: String!
updatedAt: String!
bio: String
avatar: String
followers: [User]
followings: [User]
totalFollowings: Int!
totalFollowers: Int!
}
`;
//isFollowing: Boolean!
//isMe: Boolean!
totalFollowings와 totalFollowers를 추가 하였다. 이 두개값은 DB에 없는 데이터다. 이를 보여주기 위해서는 다른 resolvers와 마찬가지로 resolver를 만들어 주어야한다.
🥑 users.resolvers.js
export default {
User: {
totalFollowings: (root) => {
console.log(root);
return 0;
},
},
};
두두둥장 드디어 베일에 쌓인(이전에 계속 _ 로 처리 되었던) root! 등장 위와 같이 처리하고 아래와 같이 kimmad 값을 넣고 console 값을 확인해보면
아래와 같이 kimmad에 포함된 모든 User값이 출력 되고 있다. 그렇다. root는 본인(여기서는 totalFollowings) 을 호출한 해당 값의 parent 값을 모두 가지고 온다
export default {
User: {
totalFollowings: ({ id }) => {
console.log(id);
return 0;
},
},
};
이를 이용해 id 값만 가지고 와보자. (여기서 {id} 는, root.id 의 값을 가지고 오는것)
🍔 핵심 내용
🥑 Computed Fields의 이해 2
이제 실제로, 해당 개념을 이용하여 전체 팔로워 수, 전체 팔로윙 수, 지금 보고 있는 화면이 내 계정인지 (내 계정이면 edit 부분 활성화), 다른 사람 계정이면 내가 팔로잉 하고 있는 사람인지, 총 4가지를 확인해보자
🍔 코드 리뷰
🥑 users.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type User {
id: String!
firstName: String!
lastName: String
userName: String!
email: String!
createdAt: String!
updatedAt: String!
bio: String
avatar: String
followers: [User]
followings: [User]
totalFollowings: Int!
totalFollowers: Int!
isMe: Boolean!
isFollowing: Boolean!
}
`;
마지막 4개(computed fields)가 추가 되었다.
🥑 users.resolvers.js
import client from "../client";
export default {
User: {
totalFollowings: ({ id }) =>
client.user.count({
where: {
followers: {
some: {
id,
},
},
},
}),
totalFollowers: ({ id }) =>
client.user.count({
where: {
followings: {
some: {
id,
},
},
},
}),
isMe: ({ id }, _, { loggedInUser }) => {
if (!loggedInUser) {
return false;
}
return id === loggedInUser.id;
},
isFollowing: async ({ id }, _, { loggedInUser }) => {
if (!loggedInUser) {
return false;
}
const exists = await client.user.count({
where: {
userName: loggedInUser.userName,
followings: { some: { id } },
},
});
return Boolean(exists);
},
},
};
① followings 전체 수와, followers 전체 수를 확인 하는 로직은 유사 하다. seeProflie에서는 userName이 들어가야 하는데, 해당 userName의 고유 id를 확인 하여 followings같은 경우는 전체 db에서 followers 수를 찾으면 되고, 반대로 followers 같은 경우는 전체 db에서 followings 수를 count함수를 통해 알아 내면 된다.
② isMe 는 입력한 userName과 token으로 로그인된 id와의 일치여부에 따라 boolean으로 리턴값이 정해진다.
③ isFollowing 은 token값으로 로그인 된 계정의 db를 먼저 찾은 후, 입력된 username의 id 값을 내가 팔로잉 하고 있는지 확인해보고 있으면 1 --> true값 리턴, 없으면 0 --> false 값을 리턴해준다.
🍔 핵심 내용
🥑 검색어를 이용하여 users찾기 기능 만들기
🍔 코드 리뷰
🥑 searchUsers.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type Query {
searchUsers(keyword: String!): [User]
}
`;
바로 User값이 리턴됨
🥑 searchUsers.resolvers.js
import client from "../../client";
export default {
Query: {
searchUsers: async (_, { keyword }) =>
client.user.findMany({
where: { userName: { startsWith: keyword.toLowerCase() } },
}),
},
};
startsWith로 조회되게 하였고, 용도에 따라 아래 값중 하나를 사용 하면된다. db에 값이 소문자로 저장되게 하였다면, toLowerCase로 검색어가 자동으로 소문자로 들어가게 하여 검색어 대,소문자 = db 대,소문자 값을 일치 시켜 주어야 한다.