08:39 입실
이제 정글피디아 AI 세세한 디테일 보정하고,
진짜 테스트 코드 짜보기!
확실히 RDB는 엔티티 변경이 조심스러움.
그래도 몽고DB쓸 때 무작정 DB스키마를 변경하는 건 안 좋은 습관 같음.
URL의 쿼리스트링에서 대소문자 구분은 URL에 따라 다를 수 있지만, 대부분의 웹 서버와 프레임워크는 쿼리스트링의 파라미터 이름에서 대소문자를 구분합니다. 따라서, 쿼리스트링의 키를 정의할 때는 일관성을 유지하는 것이 중요합니다.
엔티티의 필드명이 isPrivate인 경우, 쿼리스트링의 키 이름을 isPrivate로 그대로 사용하는 것이 좋습니다. 이렇게 하면 백엔드와 프론트엔드 간의 혼동을 줄이고, 코드의 가독성을 높일 수 있습니다.
하지만, 실제로 쿼리스트링을 정의할 때는 일반적으로 URL 컨벤션을 따르는 것이 좋습니다. URL 컨벤션에서는 대소문자보다는 소문자를 사용하는 경향이 있습니다. 따라서, isPrivate 필드를 쿼리스트링으로 보낼 때는 isprivate, is_private 또는 private과 같이 소문자로 변환하여 사용하는 것이 일반적입니다.
리액트를 쓸 수록 컴포넌트 재활용의 장점을 느낌.
다만, 어디정도까지 세부적으로 컴포넌트를 나누고 관리해야 할지는 철학과 고민이 필요할 것 같음.
맨 처음 프로젝트 생성 후에 .gitignore에 .env를 추가하지 않아서 github에 올라가 버림.
다행이 로컬 서버 주소만 있어서 문제는 없지만, 추후에는 확실하게 처리해야 할 듯.
context로 전역에서 로그인 여부를 관리하고 있음.
그런데 새로고침을 하면 기본값인 무조건 false로 로그인 상태가 감지되기 때문에 실제로 로그인 상태이지만 로그인 페이지로 바로 넘어가버림. 그 뒤에 isLoggedIn이 true로 바뀌어도 이미 로그인 페이지로 넘어가 버린 이후이기 때문에 소용이 없음.
해결은 로그인 상태 초기값을 false가 아닌 null로 바꾸고, checkAuth()에서 정확히 로그인 여부 감지한 후 true or false로 바꾸게 하고 정확히 false일 때만 로그인 페이지로 이동하는 방식으로 했더니 정상 작동함.
useEffect(() => {
if (isLoggedIn === false) {
router.push("/login");
return;
}
fetchPosts();
}, [fetchPosts, isLoggedIn, router]);
의존성 배열이 바뀔 때만 함수를 재생성하는 방법으로 useCallback을 쓸 수 있음.
사용자 프로필 페이지에서는 api주소가 바뀔 수도 있고 안 바뀔 수도 있다. 같은 페이지에 접근했을 때 함수가 재생성되는 건 불필요한 작업이다.
그래서 useCallback으로 다른 사용자의 프로필에 접근했을 때만 함수가 재생성되게 한다.
const fetchUser = useCallback(async () => {
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_SERVER_API}/users/${params.uuid}`,
{
headers: {
"content-type": "application/json",
},
}
);
const user = await response.json();
setUser(user);
} catch (error) {
console.error(error);
}
}, [params.uuid]);
이 코드는 먼저 Post객체를 가지고 온 후,
오프라인 상태의 Post에서 likeCount를 변경 후 다시 저장한다.
만약 이 중간 사이에 다른 사용자의 like작업이 있다면 전체 like 객체 수와 Post안의 likeCount 수가 맞지 않을 가능성이 있다.
그래서 차라리 likeCount필드를 쓰는게 아니라, post 쿼리가 날아갈 때 좋아요 집계 쿼리를 날리는 것으로 대체하는 거 약간의 성능 상의 이슈는 있지만 더 나은 선택이다.
async like(id: string, userId: string): Promise<Post> {
const post = await this.findOne(id);
if (!post) {
throw new NotFoundException('Post not found');
}
const user = await this.usersRepository.findOne({ where: { id: userId } });
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
const like = await this.LikesRepository.findOne({
where: { post: { id: id }, user: { id: userId } },
});
if (like) {
await this.LikesRepository.remove(like);
post.likesCount -= 1;
} else {
const newLike = this.LikesRepository.create({
user: user,
post: post,
});
await this.LikesRepository.save(newLike);
post.likesCount += 1;
}
await this.postsRepository.save(post);
return post;
}
}
근데 이 방법으로는 게시글 리스트에서 추천 수를 보여주려면 모든 게시글마다 추천수 쿼리를 보내야 하니까, 차라리 추천을 할 때마다 포스트의 추천수를 최신화 방식이 좋을 듯함.
이렇게 하면 단순 게시글 조회시에는 별도의 추천 수 쿼리를 하지 않으니까, 전체적인 쿼리 횟수를 따져보면 이 방식이 성능 상의 이점이 있을 거라고 생각됨.
(게시글 조회가 추천 액션보다 더 빈번하게 발생할 것이므로)
하지만 해결2의 방법도 결국 좋아요가 발생하면 최신화를 시켜주어서 값을 일치시켜주는 것일 뿐, 동시성 문제가 완전히 해결되는 것은 아니다.
@Transaction() 데코레이터로 각 작업을 하나의 트랜잭션으로 묶어서 원자성을 보장한다.
async like(id: string, userId: string): Promise<PostLike> {
const post = await this.findOne(id);
if (!post) {
throw new NotFoundException('Post not found');
}
const user = await this.usersRepository.findOne({ where: { id: userId } });
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
const like = await this.postLikeRepository.findOne({
where: { post: { id: id }, user: { id: userId } },
});
if (like) {
await this.update(id, { likesCount: post.likesCount + 1 }, userId);
return await this.postLikeRepository.remove(like);
} else {
await this.update(id, { likesCount: post.likesCount - 1 }, userId);
const newLike = this.postLikeRepository.create({
user: user,
post: post,
});
return await this.postLikeRepository.save(newLike);
}
}
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
@Injectable()
export class PostsService {
constructor(
private readonly postsRepository: Repository<Post>,
private readonly usersRepository: Repository<User>,
private readonly postLikeRepository: Repository<PostLike>,
private dataSource: DataSource, // DataSource 주입
) {}
async like(id: string, userId: string): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const post = await queryRunner.manager.findOne(Post, id);
if (!post) {
throw new NotFoundException('Post not found');
}
const user = await queryRunner.manager.findOne(User, { where: { id: userId } });
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
const like = await queryRunner.manager.findOne(PostLike, {
where: { post: { id: id }, user: { id: userId } },
});
if (like) {
post.likesCount -= 1;
await queryRunner.manager.save(post);
await queryRunner.manager.remove(like);
} else {
post.likesCount += 1;
await queryRunner.manager.save(post);
const newLike = queryRunner.manager.create(PostLike, {
user: user,
post: post,
});
await queryRunner.manager.save(newLike);
}
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err; // 오류를 다시 발생시켜 호출자에게 전달
} finally {
await queryRunner.release();
}
}
}
하지만 이 코드조차도 완전히 동시 접근을 방지하는 건 아님.
좋아요 작업이 자주 발생하지 않는 다는 점에 착안해서, 좋아요가 일어날 때마다 최신 좋아요를 집계 쿼리로 모아서 post의 likesCount를 최신화 시켜주는 것.
이 경우 혹시 과거에 일부 post의 likeCount가 최신화 되지 않는 문제가 있더라도 최신의 like 액션이 발생하면 최신화 가능성이 올라가 오류의 가능성을 줄여 줌.
async like(id: string, userId: string): Promise<PostLike> {
const post = await this.findOne(id);
if (!post) {
throw new NotFoundException('Post not found');
}
const user = await this.usersRepository.findOne({ where: { id: userId } });
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
const like = await this.postLikeRepository.findOne({
where: { post: { id: id }, user: { id: userId } },
});
const likesCount = await this.postLikeRepository.count({
where: { post: { id: id } },
});
if (like) {
await this.update(id, { likesCount: likesCount + 1 }, userId);
return await this.postLikeRepository.remove(like);
} else {
await this.update(id, { likesCount: likesCount - 1 }, userId);
const newLike = this.postLikeRepository.create({
user: user,
post: post,
});
return await this.postLikeRepository.save(newLike);
}
}
트래픽이 많아지면 이런식으로 트랜잭션을 적용해야 할 것 같다. 아직 코드를 정확히 이해 못해서 최종 코드에는 반영하지 않기로 함!
create과 save가 각각 단계로 이루어짐.
const newLike = this.LikesRepository.create({
user: user,
post: post,
});
await this.LikesRepository.save(newLike);
이 두 메서드를 분리함으로써 TypeORM은 데이터 생성과 저장을 명확하게 구분합니다. 이 방식은 코드의 가독성과 유지 보수성을 높이며, 개발자가 데이터베이스 작업을 더 세밀하게 제어할 수 있게 해줍니다.
예를 들어, 엔티티 인스턴스를 생성한 후에 추가적인 조작이나 검증을 수행할 수 있고, 그 후에 이를 데이터베이스에 저장할 수 있습니다. 이러한 접근 방식은 복잡한 비즈니스 로직에서 특히 유용하며, 데이터 무결성과 애플리케이션의 안정성을 높이는 데 기여합니다.
TypeORM으로 모델과 연관된 repository를 쓸 때는 module에 imports해 줘야 함.
@Module({
imports: [
TypeOrmModule.forFeature([Post, User, Comment, PostLike, PostDislike]),
AuthModule,
CommentsModule,
LangchainModule,
],
controllers: [PostsController],
providers: [PostsService, CommentsService, LangchainService],
exports: [TypeOrmModule],
})
export class PostsModule {}
데이지 UI 메인에 내가 있는 이유는?!
난 데이지UI 컨트리뷰터이기 때문이지.