This message was deleted.
# 잡담
s
This message was deleted.
u
말씀하신 부분 때문에 프래그먼트 찾아봤는데 다 resusable 얘기 밖에 안하던데 Reusable 필요없고 깊이가 1단계인 쿼리에 대해서도 fragment 가 필요하나요?
h
정확히는 colocated fragments 가 필요하고 "colocated fragment", "fragment colocaiton" 등으로 검색하면 좀 나올거같은데요
👍 2
아님 Relay fragment container 라던가
일단 여기서 간단하게 설명하려고 노력해볼게요
fragment 라고 하면 쿼리의 특정 부분을 독립적으로 기술하고 나중에 재조합 할 수 있는 단위인데요. 이걸 UI 컴포넌트 구성할 때 컴포넌트 하나 당 한 개 내지 몇 개씩 같이 "선언" 함으로써 컴포넌트와 그 컴포넌트가 가지는 데이터 의존성을 같이 배치하는걸 co-location 이라고 해요.
u
넴 먼가 프래그먼트 - 컴포넌트 응집성을 높이는 느낌이 나네요
h
co-location 을 하게 되면 자연스럽게 UI 의존성과 데이터 의존성이 일치하기 때문에 가능해지는게 "선언적 데이터 페칭" 이라고 하고요
자연스럽게 의존성 역전이 일어나는 건 덤입니다
쨋든 이걸 하면 어떻고 안하면 어떻게 되냐를 예로 들어드리면
간단한 TodoItem CRUD를 한다고 쳤을 때
Copy code
query {
  todos(first: 10) {
    id
    title
    description
  }
}
뭐 매번 이런 쿼리를 판단해서 예를들면 list container 컴포넌트 같은데 짱박아 둘 수 있을텐데요
이럼 순서가 list 에서 필요한 데이터를 자체적으로 판단해서 패칭해온 다음 그걸 item 에 driling 하는 식으로 개발될거에요?
근데 저희가 fragment container 만 사용한다고 가정하면 그걸 "애초에 신경쓰지 않게" 만들 수가 있습니다
u
todos.map(todo => <item des={todo.description} />)
이런거 말씀하시는건가여?
이럼 순서가 list 에서 필요한 데이터를 자체적으로 판단해서 패칭해온 다음 그걸 item 에 driling 하는 식으로 개발될거에요?
요게 정확히 어떤 문맥인지 이해가 안되네여
h
Copy code
fragment TodoItem_item on TodoItem {
  id
  title
  description
}
뭐 이런걸 만들어서 TodoItem 과 선언을 함께 두는거죠
Todo로는 설명이 좀 어렵나
u
Copy code
{
  ...TodoItem_item
}
이런 식으로 둔다는 건가여?
h
네 일단 그게 맞고
근데 List 도 생기고 뭣도 생기고 하다 보면
u
h
컴포넌트가 컴포지션되는것과 동일하게
프래그먼트를 컴포지션하게 되는데
프래그먼트가 잔뜩 있는 상황을 가정해볼 수 있겠죠
u
그쳐
h
필드가 뭐가 필요한지 일일히 추적하지 않아도 굉장히 자연스럽습니다. 그냥 컴포넌트를 컴포지션한 것 그대로 컴포넌트 주변의 프래그먼트를 컴포지션하면 되거든요
일단 1. 관리 측면에서 훨씬 개선이 있고요
2. 중복이나 미스가 절대 발생하지 않게 됩니다
그리고 이제 여기서 createTodo 같은 뮤테이션을 정의한다고 쳐볼게요
mutation 은 이론적으로 커맨드 + 사이드이펙트인데
사이드 이펙트 부분은 리졸버 실행순서만 제외하면 쿼리랑 역할이 동일합니다
그럼 이런걸 만들 수 ㅣㅇㅆ죠
Copy code
mutation {
  createTodo(...) {
    todos {
      ...TodoItem_item
    }
  }
}
(일단 예시입니다)
이런 관계로 원래 colocation 이 잘 적용된 코드베이스에선 데이터페칭에 따른 사이드이펙트 처리가 거의 다 생략될 수 있습니다
mutation response 를 직접 다루지 않아요
근데 주변에서 본 코드베이스는 대부분 그렇지 않더라고요
...
어쨋든 마찬가지로 Subscription 도 동일하고
u
뭔가 더 공부해야 알것 같지만 https://dev.to/ricardoromox/colocated-fragments-organizing-your-graphql-queries-in-react-24a6 이 링크랑 비슷한 건가여?
response 를 직접 안 다루고 props 로 바로 오는 방식?
h
아뇨... 흠
아 네 프래그먼트 컨테이너는 이 글에 있는거처럼 사용되던게 맞고요
u
음 근데 뮤테이션이면 사용자 인터랙션에 따라 실행되지 않나요?
h
근데 API가 중요하다기보단..
이벤트 출처는요 네
u
예시 코드만 있어도 이해가 빨리 될 것 같은데 혹시 관련 프로젝트 아시나여… 아니면 좋은 글이라도..
h
그러게 말입니다
제가 회사를 다니면 그냥 회사 코드를 던졌을것인데
허허
u
쿼리 + 컴포넌트 가 응집되는 건 알겠는데 뮤테이션이 잘 이해가 안되네요
h
@Tony Won 혹시 적당한 예제 갖고 계신거 있습니까 ㅋㅋ
그니까 아폴로 써보셨으면 아시겠지만
뮤테이션 호출후에 뷰 일관성 맞추려면 똑같이 응답 바디를 기술할 필요가 있습니다
Typename 과 id 를 기반으로 추적하고 일치하는 노드가 있으면 in-place update가 되죠
UI 변경사항 전파되고요
u
h
mutation body 가 이런 사이드이펙트에 대한 선언 입니다
물론 필드 꺼내서 핸들링 하고 싶을 수 있지만 그건 부차적인거구요
이 부분을 이해했을 때 이제
View 일관성을 맞추기 위한 의존성 단위는 이미 프래그먼트로 단위로 잘 정리가 되어 있습니다
그럼 그걸 같이 쓰는게 현명하다는거죠
애초에 프래그먼트가 같이 배치되어 있는 이상 필드를 일일히 기술할 필요가 없습니다
u
보통 mutation 은 응답없이
id
만 가지고 오는데 공통부분이 많으니까 이걸 프래그먼트로 해결하는 건가요?
h
아뇨 그게 틀려요
Mutation output 타입은
사이드이펙트가 발생할 수 있는 부분을 충실히 기술해야하고요
그래야 "선언적 데이터 패칭" 이 가능합니다
Mutation output 에 id 만 적는건
GraphQL Client 가 해줄 일을 개발자한테 넘기는거에요
u
여기서 말하는 뮤테이션의 사이드 이펙트가 뭔가요?
h
예를들면 위의 createTodo 라는 "뮤테이션"은 데이터 캐시에 어떤 부분의 변경을 초래할 수 있을까요
일단 이름이 커맨드가 아니라 "뮤테이션" 인 부분에 주목하시고요
일반적으로 GraphQL 클라이언트 안쓸 때 어떻게 처리하냐면
필요한 리소스를 fetch 함 - 응답 데이터를 전처리하고 - UI 에 반영함 이런 일련의 사이드이펙트를 기술합니다 맞죠?
u
h
뮤테이션이라는건 이 과정을 구조적으로 선언할 수 있는 DSL이에요
createTodo 라는 커맨드가 todos 의 끝에 todo를 하나 추가하는 변경을 일으킨다면
createTodo Mutation 필드의 output 은 이걸 선언할 수 있게끔 모델링이 될 수 있겠습니다
이건 페이지네이션 쿼리라 예시가 적절하지 않을 수도 있는데
그냥 그렇다고 치고 일단
그럼 만약에 기능이 추가되서
사용자 프로필 옆에 현재 todo count 를 보여주는 UI가 추가됐다고 칩시다
Copy code
type User {
  currentTodos: Int!
}
이렇게요
u
h
스키마 디자인은 이런 일련의 제품에서 데이터가 흐르는 과정을 디자인 하는 겁니다
createTodo 라는게 발생하면 화면에선
사용자 프로필에 붙어있는 카운트가 1 올라간다는 사실을
알고 있습니다 그러면 뮤테이션 아웃풋은 이렇게 바뀌어야겠죠
t

https://www.youtube.com/watch?v=r7M9B_dEbCI

요거 한번 보시죠 ㅎㅎ
👍 1
h
Copy code
type CreateTodoOutput {
  user: User!
  # ..원래 있던 todo 쪽 필드
}
여기 User가 왜 들어가냐, 데이터레이어에서 보건데 createTodo 가 User 필드의 데이터를 바꾸기 때문입니다
그럼 실제 뮤테이션 요청은 이렇게 바뀔것입니다
Copy code
mutation {
  createTodo(...) {
    user {
      ...UserProfile_user
    }
    todo @append {
      ...TodoItem_item
    }
  }
}
그리고 createTodo 가 완료되면 클라이언트의 캐시가 업데이트되고 화면에 전파되는 과정에서
u
그러면
todo
의 개수는 쿼리로 가져오는게 아닌가요?
h
UserProfile_user 에 의존하고 있는 UserProfile UI가 변경되는게 보장되기 때문에
추가적인 처리가 필요없게 되죠
쿼리로도 가져왔겠죠
(화면에 있다면)
u
쿼리로 가져와도 UI 변경이 보장되는데 뮤테이션에서도 가져오는 이유가 이해가 안됩니다..
h
안가져오면 어떻게 하실까요
u
안 가져오면 어떤 문제가 있나요?
h
UI 변경이 보장되지 않습니다
직접 손으로 업데이트 해야 됩니다
u
쿼리에 따른 응답이 오면 리렌더가 되지 않나요?
h
그럼 적어도 직접 쿼리를 재호출 하도록 처리하시겠군요
u
보통은
const { data, refetch } = useQuery()
이런 식이죠?
h
refetch는 거의 안쓰지만, 네
u
그럼 이걸 뮤테이션으로 해결하는 건가요?
h
뮤테이션에 선언하는거죠 이 요청이 가면 이 캐시에서 이 데이터가 뮤테이트 될 것 이다
u
뮤테이션 완료가 되면 refetch 하게 처리했었지만..
h
그럼 나머진 클라이언트가 알아서 해줍니다
u
그럼 여기서 캐시 업데이트를 해주는 저 필드 들이 중복될 수 있으니 그걸 프래그먼트로 만들자?
h
아뇨 ㅇ애초에 중복되는지 빠진게 있는지 이런걸 신경쓸 필요가 없고
그건 쿼리에서 프래그먼트 쓸 때와 이유가 같습니다
u
컴포넌트와의 응집성인가요
h
네네
왜냐면 UI 컴포넌트와 정확히 동일한 구조로 컴포지션되는걸 패턴이 보장하니까요
아 뮤테이션은 좀 다를 수 있긴 하네요
어쨋든 적어도 영향을 받는 컴포넌트가 무엇인지 정도만 판단하면 되지요
필드를 직접 쓰지 않을거기 때문에 ㅎㅎ..
u
필드를 직접 쓰지 않는다는 말씀이 실제 쿼리는 모두 프래그먼트로 처리하기 때문에 안 쓴다는 건가여?
h
그냥 추가적으로 필드를 더 쓸 이유가 남아있지 않기 때문입니다
기본적으로 "선언하고, 잊어버린다" 식으로 데이터 패칭 처리를 일임할 수 있습니다
u
그럼 아까 말씀하신 것 중에 절대 중복이 발생하지 않는다고 하셨는데 컴포넌트에 프래그먼트 주입할 때 실수로 동일한 필드들을 프래그먼트에 쓰는 다른 컴포넌트들이 있으면 어떻게 되나여?
h
거기에 "실수" 가 없음을 보장해주죠
u
apollo 와 같은 클라이언트가 알아서 중복을 제거해주나요 설마?
h
물론 컴포넌트 하나 단위로는 자기가 필요한 의존성을 스스로 갖고 있는 정도의 책임이 있겠지만
Fragment 는 원래 병합되면 필드 중복은 제거합니다
알아서 중복을 제거한다기보단
애초에 중복이 발생하지 않습니다
u
각각의 다른 컴폰넌트들이 다른 프래그먼트로 쿼리 요청을 할텐데 애초에 중복이 발생하지 않는다는 건 그 프래그먼트들이 모두 병합되나요?
h
프래그먼트단위로 격리했기때문에... 어떤 프래그먼트에 어떤 필드가 들어있는지 이런 디테일은 신경쓸 필요가 없어요
음 요청레벨에선 추가적인 최적화를 할지 말지라 클라이언트 구현이 결정할 일이긴 한데
병합 안해도
문제가 안생겨요
그니까
Copy code
fragment A on Todo {
  name
}
fragment B on Todo {
  name
}
query {
  todo(id: 1) {
    ...A
    ...B
    name
  }
}
이건 원래 아주 자연스러운 GraphQL 요청입니다
u
예를 들어,
<Component A />
의 쿼리 요청이
Copy code
fragment A on Todo {
  name
  gender
  address
}
이고
<Component B/>
의 요청이
Copy code
fragment B on Todo {
  name
  gender
  height
}
라고 하면
name
이랑
gender
가 중복인데 이런 컨텍스트가 아닌건가여?
h
여기에 이제 Abstract type 같은 예시를 들면 fragment 가 훨씬더 매력적인데
네 상관없어요 원래 허용됩니다
그니까 컴포넌트에선 자신이 무엇을 필요로하는지만 신경쓰고
컴포지션에선 컴포지션 자체만 신경쓰면 되겠죠
u
그러면 “중복이 발생하지 않는다” 에서 “중복” 은 어떤 걸 얘기하시는 건가요?
역으로 “중복이 발생한다” 면 어떤 게 있을까요?
h
프래그먼트가 없었다면 어디에 어떤 필드가 필요한지는 직접 추적하셔야겠지요
u
프래그먼트도 필요한 필드를 적어줘야 되지 않나여?
위 코드 처럼여
h
어디서 적느냐가 다르죠
u
컴포넌트에서 적느냐 아니면 다른 쿼리 파일에서 적느냐 이런 건가요?
h
네 핵심적인 아이디어는 데이터가 필요한 그 위치에서 관리한다 이기 때문에
u
그렇게 생각하면 컴포넌트 파일에서 프래그먼트 없이 쿼리를 작성하면여?
h
그건 같죠 이건 재귀적인거라
u
Copy code
const exampleQuery = gql`
  어떤 쿼리
`;

const ExampleComponent = () => {
  const { } = useQuery(exampleQuery)
};
여기서 어떤게 재귀적인건가요?
h
컴포넌트가 컴포넌트의 조합이 아니라면 뭐 그럴수 있겠네요 그건 관심사 차이
근데 보통 쿼리렌더러는 굉장히 큰 단위에서 구성하기 때문에
그 구성이 다른 더 작은 구성의 구성이 아니라는 점을 부정하긴 어려우실거고
u
보통은 그런것 같네요 쿼리 날린다음 자식 컴포넌트들한테 props 로 넘겨주니까
컴포넌트 컴포지션일 경우 프래그먼트가 아닌 단일 쿼리로 관리하게 될 경우 내부 컴포넌트에서 자신이 어떤 데이터를 쓰는지 확인할 수 없다?
이런 건가요?
h
납득 안되는 부분들을 최대한 알려주시기 바랍니다... 글에 대한 피드백이 됩니다 하하
아뇨 확인할 수 없다가 아니라
개발자가 일일히 관리하는거죠
u
각 컴포넌트가 뭐 쓰는지 확인해야 하니까여?
h
그리고 그렇게 개발된 코드베이스에선
u
넴넴
h
뮤테이션 호출 하고 화면이 업데이트가 덜 되는 경우가 굉장히 빈번하게 발생되는데
경험 하셨을거라고 예상함...
u
그런 경우가 많긴 했던 것 같아여
오 뭔가 득도를 한것 같습니다
깨달음 주셔서 감사합니다
h
네 GraphQL 이 일관성 문제를 자동으로 해결해 줄 수 ㅣㅇㅆ다는건 거짓말입니다
그건 원래 Relay 파트에요
물론 Apollo 도 그 아이디어를 차용해서 만들어진 캐시 스토어를 들고 있기 때문에
패턴은 동일하게 따라할 수 있습니다
u
그 일관성이란게 어떤거 말씀하시는 건가여?
h
방금 예시든
업데이트가 바로 전파되지 않는 문제요
패턴을 따라도 개발자가 직접 챙겨야하는건 맞지만
그걸 어떻게 하느냐가 많이 차이가 나게 됩니다
t
ㅋㅋㅋㅋㅋㅋㅋ 일관성은 relay 문서에 thinking in relay 읽어보시죠
h
스키마를 잘 만들었다면 그냥 빼먹은 선언만 추가하면 될거고
아니라면 "구현"을 추가로 하게 되겠죠
(모든 사이드이펙트 관리를 직접 하고 있으므로)
u
릴레이를 알아야 소통이 되니… 릴레이가 필수군요..
아폴로 진입장벽 때문에 사람들이 아폴로를 많이 쓰는 건가요?
h
음 좀 여러가지 요인이 있는데 말씀하신것도 맞습니다
t
Relay 문서내에 Thinking in GraphQL, Thinking in Relay를 꼭 읽어보세요 ㅎㅎ 새 지평이 열립니다
u
넵 꼭 읽어보겠습니당
👍 1
t
제가 공유해드린 영상도 사실 그 내용이에요 ㅋㅋ
h
Thinking in Relay 에 데이터마스킹 얘기가 있던가요
사실 데이터마스킹 이전에 패턴이 중요한거 같은데 ㅋㅋㅋㅋ
뮤테이션 예시가 있으면 좋겠는데
사실 뮤테이션에 대한 예시는 relay 공식 examples 에도 없어요
거참
암튼 뮤테이션 스키마도 굉장히 중요합니다
어떤 의미로 쿼리 스키마 디자인보다 더 중요합니다
쿼리 디자인에 공들이는건 결국 뮤테이션 디자인 잘하려고 하는거라
그냥
: ID!
는 매우 지양하시는걸 권장드려요
👍 1
쿼리만 잘써서는 react-query 같은거 대비 이점이 없어요 (relay 제외)
u
이번 기회에 fragment 로 옮겨야겠군요 아직 쿼리도 2개 밖에 없어서 다행이에요
h
abstract type 많을 때만 좀 주의하세요
apollo client 의 gql 태그가 버그가 좀 많습니다
u
넵 인지하고 있겠습니다!
h
예제로 들만한게 킹덤 사전등록 할 때 쓴 프로모션 서비스 밖에 없군요 http://npm.im/@devsisters/web-pre-registration-schema
역시 회사다닐때 팍팍 공개를 해놔야 (...)
u
Colocated fragments 는 꼭
[컴포넌트].fragments = {}
이런 식으로 할 필요는 없는거죠?
제가 맞게 이해한 건지 모르겠는데 A -> B -> C 로 컴포넌트 트리가 구성된다고 했을 때 C 에서 필요한 데이터를 프래그먼트로 적고, A에서 요청을 할 때 해당 프래그먼트를 import 해서 요청하는 방식인거죠? 인자를 넘겨야 할 경우엔
fragment
사용 못하니까
query
를 써서 아래 같이 하는 것으로 이해했습니다.
Copy code
A.fragments = {
  getDataOfC: gql`
    query getData($variable: 1) {
      ...FragmentOfC
    }
  `
  ${C.fragments.getData}
}
한 컴포넌트에서 그냥 쿼리 + lazy 쿼리 섞어 쓴다고 해도 단순하게 프래그먼트로 구성된 여러개의 쿼리를 정의하면 되니 상관없는 것 같은데 맞는지는 모르겠네요..
[컴포넌트].fragments
방식은 어차피 컴포넌트 import 해서 좋긴 한데
forwardRef
로 컴포넌트를 구성하면 없는 속성에 할당할 수 없다는 타입 에러가 나오네요 🤔
논외지만
useLazyQuery
는 Promise 를 리턴하지 않아서
client.query
로 만든 다음 써야 하는 점이 좀 불편하네요..
h
네 꼭 컴포넌트 필드에 할당하실 필요 없고요
useQuery 던 useLazyQuery 던 Promise 리턴하는걸 쓸 일이 있나요.....?
fragment 조합할 때는 Apollo gql 태그에 의존하는거보단 graphql code generator 가 생성해주는걸 쓰는게 좀 더 편해요
u
저번에 말씀드린 것처럼 defer해서 요청을 날리는데 이걸 순서를 보장하려면 Bluebird의 Promise.each 같은 걸 써야 하는데 이 때 필요합니다. 말씀하신
gql
태그에 대한 건 타입 생성 부분 말씀하시는 건가요?
graphql-code-generator 써보고 있는데
.tsx
에서
gql
쓰면 알아서 뽑아내게끔은 못하나요?
.graphql
파일로 따로 컴포넌트 별 프래그먼트를 저장해야 하는 것 같네여
https://github.com/apollographql/apollo-tooling/issues/2053 이런걸 보긴해서 넘어가는게 좋을 것 같긴 한데, apollo-codegen 을 쓰면서 딱히 불편한 점은 못 느껴서 애매하군요
h
그냥 인라인으로 쓰거나 gqless 같은걸 써도 도ㅔ긴하는데