시작하며
개발 진행 중에 에러가 났고, 꽤나 쓸데없이 삽질을 해서 화가나는 바람에 기록하고자 글을 작성한다.
TypeORM의 Date 반환
내가 사용중인 Entity는 다음과 같았다.
@Entity({
name: 'activity',
})
@Unique(['user', 'activityDate'])
export class Activity extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
name: 'activity_date',
type: 'date',
nullable: false,
})
activityDate: Date;
@Column({
name: 'view_count',
type: 'int',
nullable: false,
})
viewCount: number;
@ManyToOne(() => User, (user) => user.activities)
@JoinColumn({ name: 'user_id' })
user: User;
}
보면 activity_date 라는 컬럼이 있고, 이놈은 타입이 'date' 다. 이런식으로 명시를 해주면 실제 DB 스키마가 어떻게 생겨먹었는지 애플리케이션 레벨에서 조금 더 상세히 알 수 있다. (Typescript 만세!)
그래서 이 Activity Entity를 가지고 무얼 하느냐..?
@Injectable()
export class ActivityRepository extends Repository<Activity> {
constructor(private dataSource: DataSource) {
super(Activity, dataSource.createEntityManager());
}
...
async findActivitiesByUserIdAndYear(
userId: number,
year: number,
): Promise<Activity[]> {
const startDate = `${year}-01-01`;
const endDate = `${year}-12-31`;
return this.createQueryBuilder('activity')
.leftJoinAndSelect('activity.user', 'user')
.where('user.id = :userId', { userId })
.andWhere('activity.activityDate >= :startDate', { startDate })
.andWhere('activity.activityDate <= :endDate', { endDate })
.orderBy('activity.activityDate', 'ASC')
.getMany();
}
}
저런식으로 JOIN 후 날짜 연산을 통해 특정 데이터만을 필터링 하는 간단한 동작을 수행한다.
이렇게 들고와진 데이터들은..
...
const activities =
await this.activityRepository.findActivitiesByUserIdAndYear(userId, year);
const dailyActivities = activities.map(
(activity) =>
new DailyActivityDto({
date: activity.activityDate.toISOString().split('T')[0],
viewCount: activity.viewCount,
}),
);
return new ActivityReadResponseDto({
dailyActivities,
maxStreak: user.maxStreak,
currentStreak: user.currentStreak,
totalViews: user.totalViews,
});
Service Layer에서 저런식으로 보기좋은 날짜 형태로 변형된 다음 Dto에 담겨 클라이언트에게 이쁘게 반환된다.
그런데 문제는 여기서 발생했다.
error: TypeError: activity.activityDate.toISOString is not a function
바로 activity.activityDate의 값이 문자열 형태였던 것 이다. (문자열에는 toISOString이니 없음 ㄲㅈ ㅋㅋ)
분명히 Repsitory의 findActivityByUserIdAndYear 메서드의 반환 타입은 Activity[] 인데, 어떻게 Activity.activityDate의 타입이 문자열이 될 수가 있는 것일까?
오류 확인 및 해결
TS의 한계점이 이런곳에서 명확하게 나타난다고 생각한다. (억지 타입 탄로났죠 ㅋㅋ)
TS의 타입 선언은 컴파일시의 안전성을 위한 것일 뿐이고, 이렇게 런타임에서 발생되어 반환하는 실제 값까지는 실행 도중에 알아서 변환해주거나 오류를 발생시켜주지 않는다.
그래서 TypeORM Entity의 Column 데코레이터에 존재하는 옵션 중 transformer라는 옵션을 활용해서 activityDate 컬럼의 타입변환을 자동으로 수행토록 처리 하였다.
@Entity({
name: 'activity',
})
@Unique(['user', 'activityDate'])
export class Activity extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
name: 'activity_date',
type: 'date',
nullable: false,
transformer: {
to: (value: Date) => value,
from: (value: string | Date) => {
if (typeof value === 'string') {
return new Date(value);
}
return value;
},
},
})
activityDate: Date;
...
TypeORM은 왜 타입 지맘대로 바꿔서 반환 함?;
뭔가 의도되지 않은 동작같아서, 조금 찾아봤는데 나와 같은 의문을 가진 사람이 issue를 발행한 적이 있었고, 거기에 친절하게 답변도 달려있었다.
뭐 대충 이런 뜻인 것 같다.
"날짜가 문자열로 매핑되는거 정상임 ㅇㅇ. DBMS에서 제공하는 Date는 시간, 분, 초 이런거 없이 그냥 주는데 JS Date 에는 전체 시간이 풀로 있어야 매핑됨. 이거 때문에 옛날에 고민 좀 했는데 걍 문자열로 때리기로 함 ㅋㅋ"
'Web' 카테고리의 다른 글
자바 vs 노드 당신의 선택은?! (6) | 2025.01.18 |
---|---|
NestJS + TypeORM + Testcontainers 를 사용한 통합 테스트 DB환경 구축하기 (2) | 2025.01.17 |
코딩테스트 준비를 위한 Java 입출력 정리 (0) | 2024.05.23 |
Java record 에 대하여 (4) | 2024.03.10 |