6편. GraphQLScalar 타입 완전 정복
📚 목차
1. 기본 Scalar 종류와 의미 (Custom Scalar 소개)
2. 기본 Scalar 적용 예제 (String, Int, Boolean, Float, ID)
3. Scalar 제약과 유효성 (Resolver 유효성 검사, Custom Scalar)
4. 실무 예제 기반 입력 처리(전체 실행 흐름)
✔ 마무리 - Scalar 타입 설계와 활용 전략
GraphQL에서 Scalar(스칼라) 타입은 더 이상 세분화할 수 없는 가장 기본적인 데이터 단위입니다.
이는 SQL의 VARCHAR, INT, BOOLEAN과 같은 원시 자료형과 유사하며, 객체(Object) 타입을 구성하는 필드의 최소 단위로 사용됩니다.
Scalar 타입을 올바르게 정의하면 클라이언트와 서버 간 데이터 형식 계약을 명확히 할 수 있어, 타입 불일치나 형식 오류로 인한 버그를 사전에 예방할 수 있습니다.

📂 [GitHub 실습 코드 보러가기] (https://github.com/cericube/graphql-tutorial-server)
1. 기본 Scalar 종류와 의미 (Custom Scalar 소개)
GraphQL 표준 사양에서는 다음과 같은 5가지 Scalar 타입을 기본적으로 제공합니다.
| 타입 | 설명 | 예시 |
| Int | 32비트 부호 있는 정수 | 42, -15 |
| Float | 부동소수점 수 | 3.14, -0.5 |
| String | UTF-8 문자열 | "Hello", "안녕하세요" |
| Boolean | 참/거짓 값 | true, false |
| ID | 고유 식별자(문자열로 처리) | "a1b2c3", "1001" |
✔️특징
▸ ID : 데이터베이스 기본 키와 유사한 개념이며, 내부적으로 문자열로 처리됩니다.
GraphQL 스펙상 숫자 리터럴도 허용되지만, JSON 요청 시에는 문자열로 전달하는 것이 안전하며, 실무에서는 대부분 "12345"와 같이 문자열 형태로 사용합니다.
▸String: 일반 텍스트뿐만 아니라 JSON 직렬화 문자열, Base64 인코딩 데이터 등 다양한 형식의 문자열을 저장할 수 있습니다.
▸기본 Scalar 타입은 데이터 형식만 보장하므로, 값의 범위나 패턴을 강제하려면 Custom Scalar를 구현해야 합니다.
✔️ 자주 쓰이는 Custom Scalar 예시
▸ DateTime → 날짜·시간 형식 강제
▸ Email → 이메일 형식 검증
▸ PositiveInt → 양수 정수만 허용
2. 기본 Scalar 적용 예제 (String, Int, Boolean, Float, ID)
GraphQL의 기본 Scalar 타입은 각 필드가 어떤 데이터 형식을 가질지를 명확히 정의합니다.
이는 데이터 무결성을 보장하고, 클라이언트와 서버 간 데이터 계약을 명시적으로 표현하는 중요한 장치입니다.
아래 예제에서는 User 타입을 정의하며, GraphQL 표준 Scalar 타입인 String, Int, Boolean, Float, ID를 모두 적용합니다.
✔️ 스키마 예시(user.schema.ts)
# User 타입 정의
type User {
id: ID! # 고유 식별자 (Non-Null)
# - ID 타입: 문자열로 처리, 식별자 용도로 사용
# - Non-Null: 값이 반드시 있어야 함
name: String! # 사용자 이름 (Non-Null)
# - UTF-8 문자열
# - 실무에서는 길이 제한/형식 검증 추가 가능
age: Int! # 나이 (Non-Null)
# - 32비트 정수
# - 음수나 비정상 범위는 별도 검증 필요
height: Float! # 키 (Non-Null)
# - 소수점 포함 숫자
# - 예: 175.5(cm)
isActive: Boolean! # 활성 여부 (Non-Null)
# - true/false 값
# - 계정 활성화, 재고 여부 등 상태 표시 용도로 활용
}
# Query 타입 정의
type Query {
# 사용자 정보를 ID로 조회
getUser(id: ID!): User
}
✔️ 요청 Query 예시
query {
getUser(id: "u123") {
id
name
age
height
isActive
}
}
▸ id는 "u123"처럼 문자열 형태로 전달
GraphQL 스펙상 숫자 리터럴도 허용되지만, JSON 요청에서는 문자열이 안전
▸ 필요한 필드만 선택적으로 조회 가능 → 불필요한 데이터 전송 방지
✔️ 응답 예시
{
"data": {
"getUser": {
"id": "u123",
"name": "홍길동",
"age": 29,
"height": 175.5,
"isActive": true
}
}
}
▸ id: "u123" → 문자열로 표현된 고유 식별자(ID 타입)
▸ name: "홍길동" → UTF-8 문자열(String 타입)
▸ age: 29 → 정수(Int 타입)
▸ height: 175.5 → 소수점을 포함하는 수(Float 타입)
▸ isActive: true → 활성 상태(Boolean 타입)
✔️ 실무 활용 팁
1. Non-Null 적극 활용
▸ 필수 데이터에는 !를 붙여 데이터 계약을 강제하면, 클라이언트 개발 시 누락 오류를 방지할 수 있습니다.
2. 필드 타입 설계 시 API 사용 패턴 고려
▸ 예: 금액 필드 → 부동소수점(Float)보다 정수(Int)로 최소 단위(원, 센트) 저장을 권장
3. ID 타입은 항상 문자열로 전달
▸ JSON 직렬화 시 숫자→문자 변환 과정에서 오류를 방지하고, DB 키 유형 변경에도 유연하게 대처 가능
3. Scalar 제약과 유효성 (Resolver 유효성 검사, Custom Scalar)
GraphQL의 기본 Scalar 타입은 데이터 형식만 보장하며, 값의 범위나 패턴은 보장하지 않습니다.
예를 들어, age: Int는 정수라는 점만 확인하고, 음수이거나 상식적으로 말이 되지 않는 값(-5, 999)도 허용합니다.
이러한 한계를 보완하려면 두 가지 접근 방식을 사용할 수 있습니다.
🔸 Resolver 레벨에서 유효성 검사 : 비즈니스 로직 단계에서 직접 값 검증
🔸 Custom Scalar 타입 정의 : GraphQL 타입 자체에 검증 로직을 포함하여, 잘못된 값이 Resolver로 전달되지 않게 함
🔷 1) Resolver 레벨 유효성 검사
✔️ user.service.ts
export class UserService {
/**
* 사용자 생성 로직
* @param name 사용자 이름
* @param age 사용자 나이
* @returns 생성된 사용자 객체
*/
createUser(name: string, age: number) {
// 나이 값 검증: 0세 이상, 120세 이하만 허용
if (age < 0 || age > 120) {
throw new Error('나이는 0~120세 범위여야 합니다.');
}
// height와 isActive는 기본값 설정
return {
id: 'u124', // 서버에서 생성된 고유 ID
name, // 입력된 이름
age, // 검증된 나이 값
height: 170.0, // 기본 키 값
isActive: true, // 기본 활성 상태
};
}
}
▸ Service 계층에서 범위 검증을 수행해, 비정상적인 값이 DB에 저장되지 않도록 방지합니다.
▸ 이 방법은 구현이 간단하지만, 모든 유효성 검증이 Resolver 또는 Service 로직에 흩어질 수 있다는 단점이 있습니다.
▸ 같은 필드의 검증 로직을 여러 Resolver에서 반복 구현해야 할 수도 있습니다.
🔷 2) Custom Scalar로 형식/범위 제한
✔️ positiveInt.scalar.ts
import { GraphQLScalarType, Kind } from 'graphql';
/**
* PositiveInt Scalar
* - 0보다 큰 정수만 허용하는 GraphQL Scalar 타입
* - 값이 잘못되면 GraphQL 레벨에서 요청을 거부
*/
export const PositiveInt = new GraphQLScalarType({
name: 'PositiveInt', // GraphQL에서 사용할 타입 이름
description: '양수 정수만 허용하는 Scalar 타입',
// 클라이언트 → 서버 (변수로 전달 시) 값 검증
parseValue(value) {
if (typeof value !== 'number' || value <= 0) {
throw new Error('양수 정수만 입력 가능합니다.');
}
return value;
},
// 서버 → 클라이언트 (응답 시) 직렬화
serialize(value) {
return value;
},
// 클라이언트 → 서버 (리터럴 값) 값 검증
parseLiteral(ast) {
if (ast.kind === Kind.INT && parseInt(ast.value, 10) > 0) {
return parseInt(ast.value, 10);
}
throw new Error('양수 정수만 입력 가능합니다.');
},
});
▸ PositiveInt 타입은 GraphQL 스키마 레벨에서 유효성 검증을 수행합니다.
▸ 클라이언트에서 잘못된 값을 보내면 Resolver 로직이 실행되기 전에 요청이 거부됩니다.
▸ 이렇게 하면 동일한 필드 검증 로직을 여러 곳에 중복 작성할 필요가 없습니다.
▸ 단, Custom Scalar를 도입하면 스키마와 Resolver 등록 과정이 조금 더 복잡해집니다.
✔️ 스키마 적용 예시 (user.schema.ts)
scalar PositiveInt
type User {
id: ID!
name: String!
age: PositiveInt! # 나이에 양수 제약 적용
height: Float!
isActive: Boolean!
}
AST(Abstract Syntax Tree)란?
- Abstract Syntax Tree는 "추상 구문 트리"라는 뜻입니다.
- 사용자가 작성한 GraphQL 쿼리 문자열을 파싱한 결과를 트리 형태로 표현한 것입니다.
- 이 트리 구조의 각 요소(노드)가 쿼리의 구성 요소를 나타내며,
- 예를 들어 INT, STRING, BOOLEAN과 같은 타입, 리터럴 값, 필드 이름 등이 각각 하나의 AST 노드가 됩니다.
✔️ 실무 활용 전략
▸ 단순 범위 제한 → Resolver(Service) 레벨에서 처리
▸ 형식 강제(예: 이메일, 날짜, 양수 등) → Custom Scalar로 처리
▸ 이렇게 분리하면 코드 가독성이 높아지고, 검증 로직을 재사용하기 쉬워집니다.
4. 실무 예제 기반 입력 처리(전체 실행 흐름)
이 절에서는 앞서 학습한 기본 Scalar 타입, Custom Scalar, 그리고 유효성 검사 로직을 하나로 통합하여 실제로 동작하는 GraphQL 서버를 구현합니다.
이 예제는 단순한 개념 시연을 넘어 실무 환경에서 바로 실행 가능한 형태로 설계되었으며, 스키마 정의부터 서버 구동까지 엔드투엔드(End-to-End) 흐름을 모두 포함합니다.
🔷 구현 범위
1. 스키마 정의
▸ User 타입 정의 및 각 필드에 적절한 Scalar 타입 적용
▸ 나이(age) 필드에는 PositiveInt Custom Scalar를 적용해 양수 정수만 허용
2. Custom Scalar 구현
▸ GraphQL 레벨에서 데이터 형식과 범위를 검증
▸ 잘못된 값은 Resolver에 도달하기 전에 차단
3. Resolver & Service 작성
▸ 데이터 조회 및 생성 로직 구현
▸ 비즈니스 로직 기반의 추가 검증 적용(예: 나이 범위 제한)
4. 서버 구동
▸ GraphQL Yoga를 이용하여 빠르고 간결하게 서버 실행
🔷 디렉토리 구조
ch6/src/
├── modules/
│ └── user/
│ ├── positiveInt.scalar.ts
│ ├── user.schema.ts
│ ├── user.service.ts
│ └── user.resolver.ts
├── schema.ts
├── resolvers.ts
└── index.ts
✔️ Custom Scalar 구현 - positiveInt.scalar.ts
import { GraphQLScalarType, Kind } from 'graphql';
/**
* PositiveInt Scalar
* - 0보다 큰 정수만 허용하는 GraphQL Scalar 타입
* - 값이 잘못되면 GraphQL 레벨에서 요청을 거부
*/
export const PositiveInt = new GraphQLScalarType({
name: 'PositiveInt', // GraphQL에서 사용할 타입 이름
description: '양수 정수만 허용하는 Scalar 타입',
// 클라이언트 → 서버 (변수로 전달 시) 값 검증
parseValue(value) {
if (typeof value !== 'number' || value <= 0) {
throw new Error('양수 정수만 입력 가능합니다.');
}
return value;
},
// 서버 → 클라이언트 (응답 시) 직렬화
serialize(value) {
return value;
},
// 클라이언트 → 서버 (리터럴 값) 값 검증
parseLiteral(ast) {
if (ast.kind === Kind.INT && parseInt(ast.value, 10) > 0) {
return parseInt(ast.value, 10);
}
throw new Error('양수 정수만 입력 가능합니다.');
},
});
▸ PositiveInt를 age 필드에 적용하면, 0 이하 값은 GraphQL 레벨에서 즉시 차단됩니다.
▸ 잘못된 값이 들어오면 Resolver 호출 자체가 이루어지지 않음 → 불필요한 서버 자원 낭비 방지
✔️ 스키마 정의 - user.schema.ts
// src/modules/user/user.schema.ts
/**
* User 스키마 정의
* - 기본 Scalar(Int, Float, String, Boolean, ID)와 Custom Scalar(PositiveInt)를 함께 사용
* - age 필드에는 PositiveInt를 적용해 양수 정수만 허용
* - Query와 Mutation 모두 제공하여 읽기/쓰기 기능 구현
*/
export const userTypeDefs = /* GraphQL */ `
# Custom Scalar 선언
# PositiveInt: 0보다 큰 정수만 허용하는 타입
scalar PositiveInt
# User 타입 정의
type User {
id: ID! # 고유 식별자 (문자열 처리, Non-Null)
name: String! # 사용자 이름 (UTF-8 문자열, 필수 값)
age: Int! # 나이 (정수, 필수 값) - createUser에서는 PositiveInt 사용
height: Float! # 키 (소수점 포함 가능, 필수 값)
isActive: Boolean # 활성 상태 (true/false, 선택 값)
}
# Query 타입 정의
type Query {
# 사용자 정보를 ID로 조회
# - id: 조회할 사용자 고유 ID
getUser(id: ID!): User
}
# Mutation 타입 정의
type Mutation {
# 새 사용자 생성
# - name: 사용자 이름 (필수)
# - age: 나이 (PositiveInt, 필수) → 0보다 큰 정수만 허용
# - height: 키 (Float, 필수)
createUser(name: String!, age: PositiveInt!, height: Float!): User!
}
`;
▸ Non-Null(!)로 필수 입력값임을 명시해 API 계약을 강화
▸ PositiveInt 적용 시, 클라이언트는 나이를 0보다 큰 정수로만 보낼 수 있음
✔️ Service 계층 - user.service.ts
// src/modules/user/user.service.ts
export class UserService {
// 단일 사용자 조회
getUserById(id: string) {
// 실무에서는 DB 조회 로직이 들어감
return { id, name: '홍길동', age: 29, height: 175.5, isActive: true };
}
// 사용자 생성
createUser(name: string, age: number, height: number) {
// 추가 유효성 검사 (Custom Scalar에서 막히지 않는 로직 처리)
if (age < 0 || age > 120) {
throw new Error('나이는 0~120세 범위여야 합니다.');
}
// DB 저장 로직 대신 샘플 데이터 반환
return { id: 'u124', name, age, height, isActive: true };
}
}
▸ Custom Scalar는 데이터 형식/범위 검증을, Service는 비즈니스 로직 기반 검증을 담당
▸ 이렇게 이중 방어 구조를 두면 안정성이 높아짐
✔️ Resolver 구현 – user.resolver.ts
//src/modules/user/user.resolver.ts
import { PositiveInt } from './positiveInt.scalar';
import { UserService } from './user.service';
const userService = new UserService();
/**
/**
* User 리졸버 정의
* - PositiveInt Custom Scalar 등록
* - Query, Mutation 구현
*/
export const userResolvers = {
PositiveInt, // Custom Scalar 등록
Query: {
getUser: (_: undefined, { id }: { id: string }) => {
return userService.getUserById(id);
},
},
Mutation: {
createUser: (
_: undefined,
{ name, age, height }: { name: string; age: number; height: number }
) => {
return userService.createUser(name, age, height);
},
},
};
▸ PositiveInt Scalar를 Resolver에 등록해야 스키마와 실제 로직이 연결됨
▸ 예외 발생 시 GraphQL이 에러 객체로 변환해 클라이언트로 전달
✔️ 스키마·리졸버 통합
//schema.ts
import { userTypeDefs } from './modules/user/user.schema';
export const typeDefs = [userTypeDefs];
//resolvers.ts
import { userResolvers } from './modules/user/user.resolver';
export const resolvers = [userResolvers];
✔️ 서버 구동 - index.ts
import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'http';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
/**
* GraphQL Yoga 서버 생성
* - makeExecutableSchema로 타입과 리졸버 결합
*/
const yoga = createYoga({
schema: createSchema({
typeDefs,
resolvers,
}),
});
const server = createServer(yoga);
server.listen(4000, () => {
console.log('🚀 Server ready at http://localhost:4000/graphql');
});
▸ GraphQL Yoga는 Express나 Fastify 없이 독립적으로 동작
▸ 빠른 프로토타이핑과 학습에 적합
✔️ 실행 결과
graphql-tutorial-server> npx ts-node .\ch06\src\index.ts
🚀 Server ready at http://localhost:4000/graphql
▸Query

▸Mutation

✔️ 실무 적용 팁
▸ Custom Scalar + Resolver 유효성 검사를 함께 사용하면 데이터 무결성을 이중으로 보장할 수 있습니다.
▸ API 계약(스키마)를 중심으로 개발하면, 클라이언트와 서버가 병렬로 작업 가능해 개발 속도가 빨라집니다.
▸ GraphQL Yoga는 설정이 간단해 빠른 프로토타이핑과 학습 환경 구축에 적합하며, Express·Fastify 등 다른 프레임워크로의 이전도 용이합니다.
✔ 마무리 - Scalar 타입 설계와 활용 전략
GraphQL의 Scalar 타입은 단순한 데이터 형식 지정이 아니라, API 데이터 계약을 지키는 1차 방어선입니다.
기본 Scalar를 이해하고 적재적소에 활용하면 스키마 설계가 명확해지고, 클라이언트와 서버 간 데이터 일관성이 높아집니다.
또한 Custom Scalar를 도입해 형식과 범위를 강제하고, Service 계층에서 추가 검증을 병행하면 유효성 검증과 비즈니스 규칙을 안정적으로 구현할 수 있습니다.
결국 실무에서는 “기본 Scalar + Custom Scalar + Service 검증”을 조합해 데이터 품질을 보장하고 유지보수성을 높이는 것이 가장 효과적인 전략입니다.
📂 [GitHub 실습 코드 보러가기] (https://github.com/cericube/graphql-tutorial-server)
※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.
'3.SW개발 > GraphQL 배우기' 카테고리의 다른 글
| 8편. GraphQL에서 InputObject 타입 활용 (0) | 2025.12.01 |
|---|---|
| 7편. GraphQL에서 Enum 타입 설계 전략 (0) | 2025.12.01 |
| 5편. GraphQL 스키마와 리졸버 구조 이해 (0) | 2025.11.30 |
| 4편. GraphQL 실무 아키텍처 설계: 도메인 구조와 책임 분리 (0) | 2025.11.27 |
| 3편. GraphQL Hello Query 실습: 첫 서버 구축하기 (0) | 2025.11.27 |