[크래프톤 정글 3기] 1/8(월) TIL

ClassBinu·2024년 1월 8일
0

크래프톤 정글 3기 TIL

목록 보기
83/120

08:39 입실
이제 정글피디아 AI 세세한 디테일 보정하고,
진짜 테스트 코드 짜보기!

RDB 엔티티

확실히 RDB는 엔티티 변경이 조심스러움.
그래도 몽고DB쓸 때 무작정 DB스키마를 변경하는 건 안 좋은 습관 같음.

쿼리스트링 키 값 네이밍 관련해서 GPT 답변

URL의 쿼리스트링에서 대소문자 구분은 URL에 따라 다를 수 있지만, 대부분의 웹 서버와 프레임워크는 쿼리스트링의 파라미터 이름에서 대소문자를 구분합니다. 따라서, 쿼리스트링의 키를 정의할 때는 일관성을 유지하는 것이 중요합니다.

엔티티의 필드명이 isPrivate인 경우, 쿼리스트링의 키 이름을 isPrivate로 그대로 사용하는 것이 좋습니다. 이렇게 하면 백엔드와 프론트엔드 간의 혼동을 줄이고, 코드의 가독성을 높일 수 있습니다.

하지만, 실제로 쿼리스트링을 정의할 때는 일반적으로 URL 컨벤션을 따르는 것이 좋습니다. URL 컨벤션에서는 대소문자보다는 소문자를 사용하는 경향이 있습니다. 따라서, isPrivate 필드를 쿼리스트링으로 보낼 때는 isprivate, is_private 또는 private과 같이 소문자로 변환하여 사용하는 것이 일반적입니다.

React

컴포넌트

리액트를 쓸 수록 컴포넌트 재활용의 장점을 느낌.
다만, 어디정도까지 세부적으로 컴포넌트를 나누고 관리해야 할지는 철학과 고민이 필요할 것 같음.

.env 삭제하기

맨 처음 프로젝트 생성 후에 .gitignore에 .env를 추가하지 않아서 github에 올라가 버림.
다행이 로컬 서버 주소만 있어서 문제는 없지만, 추후에는 확실하게 처리해야 할 듯.

원격 저장소 .env 삭제 과정

  1. 원격에서 삭제 후 커밋
  2. 로컬에서 기존 .env 내용 미리 복사
  3. git pull로 원격 저장소 로컬에 반영
  4. .env 다시 생성(git이 추적 안 함)

새로고침 대응

context로 전역에서 로그인 여부를 관리하고 있음.
그런데 새로고침을 하면 기본값인 무조건 false로 로그인 상태가 감지되기 때문에 실제로 로그인 상태이지만 로그인 페이지로 바로 넘어가버림. 그 뒤에 isLoggedIn이 true로 바뀌어도 이미 로그인 페이지로 넘어가 버린 이후이기 때문에 소용이 없음.

해결은 로그인 상태 초기값을 false가 아닌 null로 바꾸고, checkAuth()에서 정확히 로그인 여부 감지한 후 true or false로 바꾸게 하고 정확히 false일 때만 로그인 페이지로 이동하는 방식으로 했더니 정상 작동함.

  useEffect(() => {
    if (isLoggedIn === false) {
      router.push("/login");
      return;
    }
    fetchPosts();
  }, [fetchPosts, isLoggedIn, router]);

useCallback

의존성 배열이 바뀔 때만 함수를 재생성하는 방법으로 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]);

동시성 문제

해결 1

이 코드는 먼저 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

근데 이 방법으로는 게시글 리스트에서 추천 수를 보여주려면 모든 게시글마다 추천수 쿼리를 보내야 하니까, 차라리 추천을 할 때마다 포스트의 추천수를 최신화 방식이 좋을 듯함.

이렇게 하면 단순 게시글 조회시에는 별도의 추천 수 쿼리를 하지 않으니까, 전체적인 쿼리 횟수를 따져보면 이 방식이 성능 상의 이점이 있을 거라고 생각됨.
(게시글 조회가 추천 액션보다 더 빈번하게 발생할 것이므로)

해결 3

하지만 해결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);
    }
  }

트래픽이 많아지면 이런식으로 트랜잭션을 적용해야 할 것 같다. 아직 코드를 정확히 이해 못해서 최종 코드에는 반영하지 않기로 함!

TypeORM

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 {}

DaysiUI

데이지 UI 메인에 내가 있는 이유는?!
난 데이지UI 컨트리뷰터이기 때문이지.

0개의 댓글