8편. GraphQL에서 InputObject 타입 활용
📚 목차
1. InputObject 타입의 개념과 선언 문법
2. InputObject를 활용한 구조화 설계 전략
3. 중첩된 InputObject와 서버 처리 흐름
4. InputObject을 활용한 계정 생성 실전 예제
✔ 마무리 - 구조화된 입력 설계
GraphQL에서 클라이언트가 서버에 데이터를 전달할 때, 단일 스칼라 인자를 여러 개 나열하는 방식은 초기에는 간단해 보이지만, 실제 운영 환경에서는 점점 관리하기 어려워집니다.
입력 필드가 많아지고, 필드 간 관계나 계층 구조가 필요해지면 구조화된 입력 설계가 필수적입니다.
GraphQL에서는 이러한 요구를 해결하기 위해 InputObject 타입을 제공합니다. 이 장에서는 InputObject의 개념부터 구조화 전략, 중첩 구조 설계, 그리고 서버에서의 처리 방식까지 단계적으로 다뤄보겠습니다.

📂 [GitHub 실습 코드 보러가기] (https://github.com/cericube/graphql-tutorial-server)
1. InputObject 타입의 개념과 선언 문법
GraphQL에서는 클라이언트가 서버에 데이터를 전달할 때, 다양한 인자 전달 방식을 제공합니다.
단순한 요청이라면 String, Int, Boolean 같은 스칼라 타입을 직접 나열해도 충분하지만, 실무에서는 대부분 여러 개의 필드가 포함된 복합 구조를 다루게 됩니다.
이런 구조를 명확하게 정의하고 관리하기 위해 사용하는 것이 바로 InputObject 타입입니다.
🔷 InputObject란?
InputObject는 GraphQL에서 입력 전용 객체 타입입니다.
input 키워드로 정의하며, 일반적인 type과 달리 출력 필드에는 사용할 수 없습니다.
또한 input 타입 안에서는 기본 스칼라 타입 또는 다른 InputObject 타입만 사용할 수 있습니다.
출력용 타입(type)을 중첩하거나, 순환 참조 구조는 허용되지 않습니다.
🔷 실무 예제: CreateAccountInput 선언
회원가입 기능을 예로 들면, 사용자의 이름, 이메일, 비밀번호뿐 아니라 주소나 프로필 정보도 함께 입력받는 경우가 많습니다.
이럴 때 인자를 하나씩 나열하기보다는 InputObject로 구조화하는 편이 훨씬 효율적입니다.
# 회원 계정 생성을 위한 입력 타입
input CreateAccountInput {
name: String! # 사용자 이름 (필수)
email: String! # 이메일 주소 (필수)
password: String! # 비밀번호 (필수)
phone: String # 휴대폰 번호 (선택)
address: AddressInput # 주소 정보 (선택, 중첩된 입력 타입)
profile: ProfileInput # 프로필 정보 (선택, 중첩된 입력 타입)
}
위 예제에서 address와 profile은 다시 별도의 InputObject로 정의할 수 있습니다
# 주소 정보를 위한 입력 타입
input AddressInput {
street: String! # 도로명 주소
city: String! # 도시명
zipcode: String! # 우편번호
}
# 사용자 프로필 정보를 위한 입력 타입
input ProfileInput {
bio: String # 자기소개
website: String # 웹사이트 주소
}
▸ !가 붙은 필드는 필수입니다.
▸ 선택 필드는 생략하거나 null로 전달할 수 있으며, 서버에서 기본값을 설정할 수도 있습니다.
이처럼 구조화된 InputObject를 사용하면, 클라이언트에서는 다음과 같은 형태로 데이터를 전달할 수 있습니다.
mutation {
createAccount(input: {
name: "Jane",
email: "jane@example.com",
password: "pw12345",
profile: {
bio: "I build things"
}
}) {
id
name
profile {
bio
}
}
}
이 예시에서는 profile.bio만 전달하고 나머지 필드는 생략하고 있습니다.
선택 필드는 필요한 것만 포함하면 되며, 중첩 구조를 통해 계층형 JSON으로 직관적인 요청 구성이 가능합니다.
2. InputObject를 활용한 구조화 설계 전략
GraphQL에서는 Mutation에 전달할 인자가 많아지는 경우가 흔합니다.
초기에는 단일 스칼라 인자를 여러 개 나열하는 방식이 간편하게 느껴질 수 있지만, 인자가 늘어날수록 코드가 복잡해지고 재사용성도 떨어지며, 유지보수에 불리해집니다.
이러한 문제를 해결하는 대표적인 방법이 바로 InputObject를 활용한 구조화 설계입니다.
🔷 비추천 방식: 인자 나열 구조
type Mutation {
createAccount(
name: String!
email: String!
password: String!
phone: String
street: String
city: String
zipcode: String
): Account!
}
이처럼 필드를 하나씩 나열하는 방식은 다음과 같은 문제를 일으킵니다.
▸ 필드 수가 늘어날수록 가독성이 급격히 낮아집니다.
▸ 관련된 정보(예: 주소)를 묶지 못해 구조적으로 불안정합니다.
▸ 타입 재사용이나 테스트 코드 작성, 타입 추론 등에서 비효율이 발생합니다.
🔷 권장 방식: InputObject로 구조화
input AddressInput {
street: String!
city: String!
zipcode: String!
}
input CreateAccountInput {
name: String!
email: String!
password: String!
phone: String
address: AddressInput
}
AddressInput은 주소 관련 필드를 하나의 입력 객체로 묶은 구조입니다.
CreateAccountInput은 이 AddressInput을 포함해 사용자의 기본 정보를 함께 정의하고 있습니다.
위와 같이 관련 필드를 하나의 InputObject로 묶으면 다음과 같은 이점이 있습니다:
▸ 타입 재사용: AddressInput은 다른 Mutation이나 Query에서도 재사용이 가능합니다.
▸ 명세의 간결화: API 문서나 Playground에서 스키마가 훨씬 직관적으로 표현됩니다.
▸ 계약의 명확성: 클라이언트와 서버가 동일한 입력 구조를 공유하게 되어 데이터 정합성을 유지하기 쉬워집니다.
▸ 유지보수 편의성: 구조가 바뀌더라도 객체 단위로 관리할 수 있어 변경에 유연하게 대응할 수 있습니다.
GraphQL에서 InputObject를 활용한 구조화 설계는 단순히 필드를 묶는 것을 넘어서, API의 일관성과 확장성, 유지보수성까지 고려한 필수적인 전략입니다.
Mutation의 인자 수가 많아지기 시작하는 시점부터는, 구조화된 입력 설계를 적극 도입하는 것이 바람직합니다.
3. 중첩된 InputObject와 서버 처리 흐름
GraphQL의 InputObject는 단순한 1-depth 구조만을 다루는 것이 아닙니다.
입력 타입 안에 다른 InputObject를 중첩시켜, 복잡한 계층형 데이터를 자연스럽게 표현하고 전달할 수 있습니다.
실제 서비스에서는 사용자 주소, 프로필, 설정 값 등 다양한 데이터를 한 번에 처리해야 하는 경우가 많기 때문에, 이 중첩 구조는 실무에서 매우 자주 사용됩니다.
🔷 중첩 Input 구조 예시
회원가입 요청을 처리하는 CreateAccountInput 타입은 다음과 같이 중첩 구조로 설계할 수 있습니다.
input CreateAccountInput {
name: String!
email: String!
password: String!
address: AddressInput
profile: ProfileInput
}
중첩되는 타입 정의는 다음과 같습니다.
input AddressInput {
street: String!
city: String!
zipcode: String!
}
input ProfileInput {
bio: String
website: String
}
이 구조를 사용하면 클라이언트에서는 다음과 같은 형태로 데이터를 전달할 수 있습니다.
mutation {
createAccount(input: {
name: "Jane",
email: "jane@example.com",
password: "pw12345",
profile: {
bio: "I build things"
}
}) {
id
email
profile {
bio
}
}
}
여기서는 address는 생략되었고, profile.bio만 전달되고 있습니다.
필요한 필드만 선택적으로 포함할 수 있다는 것도 중첩 Input 구조의 장점입니다.
🔷 서버에서의 처리 방식
GraphQL Yoga v5에서는 중첩된 InputObject도 아래와 같이 단일 인자(args.input)로 받아 처리할 수 있습니다
createAccount: (_: unknown, args: { input: CreateAccountInput }): Account => {
return createAccount(args.input)
}
이렇게 입력된 객체는 중첩 필드도 그대로 포함하고 있으므로, 예를 들어 args.input.profile.bio와 같은 방식으로 원하는 값을 바로 참조할 수 있습니다.
다만, address, profile과 같이 선택 입력인 경우에는 undefined 또는 null로 들어올 수 있기 때문에, 내부 로직에서는 조건문을 통해 분기 처리가 필요합니다.
✔️ 유효성 검증은 어떻게 처리할까?
GraphQL은 입력 스키마만으로도 다음과 같은 유효성 검사를 자동으로 수행합니다
▸ 필수 필드 누락 → GraphQL에서 validation error 발생
▸ 잘못된 타입 전달 → 요청 거부 및 스키마 오류 반환
▸ 선택 필드 생략 → null 또는 undefined로 처리
따라서 단순한 필수값 검사나 타입 체크는 별도의 yup 또는 zod 없이도 처리할 수 있으며, 실제 비즈니스 로직과 관련된 정교한 검증은 서비스 레이어에서 분리해 구현하는 것이 일반적인 패턴입니다.
yup과 zod는
- JavaScript와 TypeScript 환경에서 데이터 유효성 검사를 수행하기 위한 스키마 기반 검증 라이브러리입니다.
- 둘 다 GraphQL과 자주 함께 사용되며, 특히 서버에서 들어오는 데이터의 형식과 제약 조건을 정교하게 검증할 때 쓰입니다.
✔️ Playground 테스트 예시
중첩된 Input 구조는 Playground에서도 바로 테스트해볼 수 있습니다.
예를 들어 다음과 같은 요청은 profile.bio만 포함해 계정을 생성합니다.
mutation {
createAccount(input: {
name: "Jane",
email: "jane@example.com",
password: "pw12345",
profile: {
bio: "I build things"
}
}) {
id
email
profile {
bio
}
}
}
이처럼 중첩 구조를 사용하면, 불필요한 필드를 모두 나열하지 않고도 필요한 데이터만 구조적으로 입력할 수 있어 효율적입니다.
4. InputObject를 활용한 계정 생성 실전 예제
앞서 학습한 InputObject의 선언, 구조화 설계, 중첩 구조 활용 방법을 바탕으로 이제 실제 서버를 구성하고 Playground를 통해 동작을 확인해 보겠습니다.
이번 실습에서는 다음과 같은 요구 사항을 다루는 계정 생성 기능을 구현합니다:
▸ 중첩 구조를 가진 InputObject로 사용자 정보 입력
▸ Playground에서 직접 요청 및 응답 확인
▸ GraphQL Yoga v5를 기반으로 간단한 서버 실행
1. account.schema.ts – InputObject 및 반환 타입 정의
// account.schema.ts
// GraphQL SDL을 문자열로 정의합니다.
// 해당 스키마는 사용자 계정 생성을 위한 Mutation과 그에 필요한 입력 타입(InputObject)을 포함합니다.
export const typeDefs = /* GraphQL */ `
# 쿼리 타입은 필수로 선언되어야 하므로 placeholder 용도로 _empty 필드를 포함합니다.
type Query {
_empty: String
}
# 주소 정보를 입력받기 위한 InputObject 타입입니다.
# 구조화된 입력을 통해 street, city, zipcode를 하나의 address 필드로 그룹화할 수 있습니다.
input AddressInput {
street: String!
city: String!
zipcode: String!
}
# 사용자 프로필 정보를 입력받기 위한 InputObject입니다.
# 선택 입력 필드로 bio와 website를 정의합니다.
input ProfileInput {
bio: String
website: String
}
# 사용자 계정 생성을 위한 최상위 InputObject입니다.
# 단일 필드 나열이 아닌 구조화된 입력 방식으로 처리합니다.
input CreateAccountInput {
name: String! # 사용자 이름 (필수)
email: String! # 이메일 주소 (필수, 고유값으로 사용 가능)
password: String! # 비밀번호 (필수, 해싱 대상)
phone: String # 휴대폰 번호 (선택)
address: AddressInput # 주소 정보 (선택, 중첩된 InputObject)
profile: ProfileInput # 프로필 정보 (선택, 중첩된 InputObject)
}
# Mutation의 응답으로 반환될 Account 타입입니다.
# 입력받은 정보 외에도 id 필드를 포함합니다.
type Account {
id: ID! # 계정 식별자 (자동 생성)
name: String!
email: String!
phone: String
address: Address
profile: Profile
}
# AddressInput과 구조가 동일한 Address 출력 타입입니다.
# 입력(Input)과 출력(Output)을 분리함으로써 명확한 타입 흐름을 제공합니다.
type Address {
street: String!
city: String!
zipcode: String!
}
# ProfileInput과 구조가 동일한 Profile 출력 타입입니다.
type Profile {
bio: String
website: String
}
# 계정 생성 Mutation 정의입니다.
# CreateAccountInput 객체를 단일 인자로 받고, 생성된 Account를 반환합니다.
type Mutation {
createAccount(input: CreateAccountInput!): Account!
}
`;
2. account.service.ts – 실제 계정 생성 처리
// account.service.ts
// 사용자 주소 정보에 대한 타입 정의
// GraphQL AddressInput과 매핑되며, 내부 속성은 모두 필수 필드입니다.
export interface Address {
street: string;
city: string;
zipcode: string;
}
// 사용자 프로필 정보에 대한 타입 정의
// bio, website는 선택 필드로 정의됩니다.
export interface Profile {
bio?: string;
website?: string;
}
// 계정 생성 시 입력받을 구조화된 타입
// GraphQL의 CreateAccountInput 타입과 1:1로 매핑됩니다.
export interface CreateAccountInput {
name: string; // 사용자 이름 (필수)
email: string; // 이메일 주소 (필수)
password: string; // 비밀번호 (필수, 이 예제에서는 저장하지 않지만 실무에선 해싱 필요)
phone?: string; // 휴대폰 번호 (선택)
address?: Address; // 주소 정보 (선택, 중첩 구조)
profile?: Profile; // 프로필 정보 (선택, 중첩 구조)
}
// 계정 정보를 저장하는 타입
// GraphQL의 Account 타입과 대응되며, 입력값 + 생성된 ID 포함
export interface Account {
id: string; // 고유 ID (서버에서 자동 생성)
name: string;
email: string;
phone?: string;
address?: Address;
profile?: Profile;
}
// 메모리 내 계정 저장소 (임시 DB 역할)
// 실제 DB 연동 전 테스트용으로 사용
const accounts: Account[] = [];
// ID를 자동 증가시키기 위한 카운터
let idCounter = 1;
// 계정 생성 함수
// GraphQL Resolver에서 호출되며, 입력값을 기반으로 새로운 계정 객체를 생성하고 저장한 뒤 반환합니다.
export const createAccount = (input: CreateAccountInput): Account => {
const newAccount: Account = {
id: String(idCounter++), // ID를 문자열로 생성 (GraphQL ID 타입과 일치)
name: input.name,
email: input.email,
phone: input.phone,
address: input.address,
profile: input.profile,
};
// 메모리 배열에 계정 추가 (DB 대체)
accounts.push(newAccount);
// 생성된 계정 정보 반환
return newAccount;
};
3. account.resolver.ts – Mutation resolver 구현
// account.resolver.ts
// 서비스 로직과 타입 정의를 가져옵니다.
// createAccount: 계정 생성 로직을 담당하는 함수
// CreateAccountInput: 입력 타입 정의
// Account: 반환 타입 정의
import { createAccount, CreateAccountInput, Account } from './account.service';
// GraphQL 스키마의 resolver 구현 객체입니다.
export const resolvers = {
Mutation: {
// createAccount Mutation에 대한 resolver 함수 정의
// GraphQL 스키마에서 input: CreateAccountInput! 구조로 들어오는 데이터를
// args.input으로 받아 내부 서비스 로직(createAccount)에 위임합니다.
createAccount: (
_: unknown, // 첫 번째 인자(root)는 사용하지 않으므로 unknown 처리
args: { input: CreateAccountInput } // 두 번째 인자(args)에서 input만 구조적으로 추출
): Account => {
// 실제 계정 생성 로직을 호출하고 그 결과를 반환
return createAccount(args.input);
},
},
};
4. src/schema.ts, src/resolvers.ts, src/index.ts,
참조: 6편. GraphQL Scalar 타입 완전 정복
✔️ Playground 테스트 예시
서버를 실행한 뒤, http://localhost:4000/graphql 에 접속하여 다음 요청을 실행해 보세요
mutation {
createAccount(input: {
name: "Jane Doe",
email: "jane@example.com",
password: "pw12345",
phone: "010-1234-5678",
address: {
street: "101 Main St",
city: "Seoul",
zipcode: "12345"
},
profile: {
bio: "Engineer & blogger",
website: "https://jane.dev"
}
}) {
id
name
email
address {
city
}
profile {
bio
}
}
}

✔ 마무리 - 구조화된 입력 설계
GraphQL을 실무에 도입하면 가장 먼저 고민하게 되는 것이 바로 입력 데이터를 어떻게 구조화할 것인가입니다.
이때 InputObject는 단순한 문법 요소가 아니라, API 명세의 일관성과 유지보수성, 확장성을 확보하는 데 있어 필수적인 도구입니다.
✔️ 실무 개발자가 기억해야 할 핵심 요약
🔸 InputObject는 Mutation 설계의 기본입니다.
인자 나열 방식은 작고 단순한 요청에만 적합합니다. 실무에선 InputObject 구조가 유지보수와 협업에 훨씬 유리합니다.
🔸 중첩 InputObject를 적극 활용하세요.
계층형 JSON 구조와 자연스럽게 매핑되며, 클라이언트와의 연동이 쉬워집니다.
🔸 GraphQL 타입 시스템만으로도 기본 검증은 충분합니다.
별도 라이브러리 없이도 필수값, 타입 일치 여부를 자동 검증할 수 있습니다.
📂 [GitHub 실습 코드 보러가기] (https://github.com/cericube/graphql-tutorial-server)
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'3.SW개발 > GraphQL 배우기' 카테고리의 다른 글
| 9편. GraphQL Query & Mutation 기본 구조와 CRUD 실습 (0) | 2025.12.02 |
|---|---|
| 7편. GraphQL에서 Enum 타입 설계 전략 (0) | 2025.12.01 |
| 6편. GraphQL Scalar 타입 완전 정복 (0) | 2025.11.30 |
| 5편. GraphQL 스키마와 리졸버 구조 이해 (0) | 2025.11.30 |
| 4편. GraphQL 실무 아키텍처 설계: 도메인 구조와 책임 분리 (0) | 2025.11.27 |