[새싹] 발표를 끝마치고 쓰는 2차 팀프로젝트 회고록

sm·2023년 2월 27일
0

2차 팀프로젝트 : 취얼업(Cheerly-Up)

  • 개발 기간: 2주(2023.2.11~2023.2.25)

  • 취얼업
    : 개발자 취업 준비생을 위한 공부 루틴 만들기 및 자기계발 웹사이트

  • 프론트엔드 2명, 백엔드 2명

  • 사용한 기술 스택: React(프론트), NestJS(백엔드), MySQL(DB), TypeORM
    : 새로운 기술을 사용해서 프로젝트를 진행하고자 했기 때문에, nestjs와 tyeporm, typescript를 프로젝트 진행 기간 동안 새로 배우면서 진행했다. 그렇기 때문에 시간이 그냥 nodejs와 express를 사용해서 진행하는 것보다 더 오래 걸려 여유있게 배포를 하지 못했던 점이 아쉬웠다. 외래키 설정하고 서버와 db를 연결하는 것도 nest로 처음해보는 것이었기 때문에 시간이 배로 걸렸다. 하지만 nest, ts 새로운 언어를 경험해보았기에 매우 값진 프로젝트였다고 생각한다.


내가 담당한 부분 - Backend

  • DB 설계

    : 전체적으로 사용자의 id를 4개(게시판, 댓글, 공부, 라이프 db)의 외래키로 설정하여 회원 탈퇴시 cascade하여 같이 데이터가 없어지도록 설정하였다. 아쉬운 부분은 닉네임의 경우 유저의 아이디가 프라이머리키이기 때문에 외래키로 설정하지 못한 부분이다. 따라서 다른 테이블의 유저의 닉네임을 가져올 때 추후에 user db 검증을 하는 방식 혹은 다른 방식으로 이 사용자가 실제로 존재하는지 따로 검증해야 하는 부분이지만, 시간이 부족해서 하지 못하였다. 추후 업데이트해야할 부분이다. 또한 투두리스트를 담당하는 study, life db는 날짜만 필요하기 때문에 형식을 DATE, default는 curtime()으로 설정하였고, 게시판와 User의 경우 시간도 같이 나올 수 있도록 DATETIME, default는 CURRENT_TIMESTAMP으로 설정하였다.
    그리고 사용자의 비밀번호는 bcrypt로 암호화하여 저장되기 때문에 longtext 데이터 형식으로 설정하였고, 프로필 사진의 경우 사진의 이름이 string값으로 저장되기 때문에 varchar(100)으로 설정하였다.
  • puppeteer를 이용하여 인프런 스터디 모집글 사이트 2 페이지 크롤링

: 아쉬웠던 점 - puppeteer를 처음 사용해서 크롤링을 해보았는데, 일단 크롤링 시간이 10초 정도 걸리는 것이 아쉬웠다. 아마 직접 서버에서 인프런 사이트에 접속해서 긁어오기 때문에 시간이 걸리는 것 같다. 하지만 구글링해보니 크롤링 속도를 높이는 법도 있는 것 같아 추후 개선해야할 부분이다. 그리고 headless 모드를 true로 설정하였음에도 불구하고, 맥이 아닌 윈도우의 경우 팝업창이 뜨는 문제가 발생했다. 구글링해보니 headless: true 가 잘 작동하지 않는 경우도 있는 것 같아 이부분도 추후 개선 사항이다. 또한 배포 모드로 했을 때도 크롤링 get요청 axios에러가 발생해서 작동하지 않았다. http이기 때문에 작동하지 않는 것일지 다른 설정을 해줘야 하는지 알아보아야 한다.

//scrapper.service.ts
혹시 참고하실 분이 있을까봐

@Injectable()
export class ScrapperService {
  async getDataViaPuppeteer() {
    const URL = `https://www.inflearn.com/community/studies`;

    const browser = await puppeteer.launch({
      headless: true,
      args: ['--fast-start', '--disable-extensions', '--no-sandbox'],
      ignoreHTTPSErrors: true,
    });
    const page = await browser.newPage();
    const data = [];

    for (let index = 1; index < 3; index++) {
      await page.goto(
        'https://www.inflearn.com/community/studies?page=' +
          index +
          '&order=recent',
        {
          waitUntil: 'networkidle2',
        },
      );

      const lists = await page.$$('div.question-list-container > ul > li');
      for (let i = 0; i < lists.length; i++) {
        const list = lists[i];

        const title = await list.$eval('h3', (element) => element.innerText);
        const url = await list.$eval('a', (element) => element.href);
        const badge = await list.$eval(
          'a > div > div > div.question__title > div > div > span',
          (element) => element.innerText,
        );

        const dataArr = {
          title: title,
          url: url,
          badge: badge,
        };

        data.push(dataArr);
      }
    }
    await page.close();
    await browser.close();
    return data;
  }
}

추가 업데이트) 배포 오류 해결하고 무사히 Docker 배포에 성공했다!!!

puppeteer docker 배포 관련 포스트


  • 게시판, 댓글, 투두리스트 CRUD, 회원 수정, 회원 탈퇴 기능




: Nestjs를 사용해서 클라이언트로 데이터를 전달하고, 요청받는 것이 처음에는 너무 생소해서 오류가 많이 났다. axios 요청을 주고 받는 것조차 너무 어려웠던 기억이 난다. 덕분에 postman과 insomnia를 사용하는 이유를 잘 체감하게 되었지만 말이다.
무튼 nestjs 사용에 있어서 가장 좋았던 점은 도메인을 관리하기 편했다는 점이다. controller에서 기본 도메인을 설정하면 get, post, patch, delete 등 다른 요청의 도메인은 기본 도메인에 맞춰서 설정할 수 있다. 그리고 controller, module, service로 나누어진 점도 흥미로웠다. 컨트롤러는 요청을 반환하고, 서비스는 데이터베이스와 관련된 부분을 다루는 점이 확실히 분리되어 있어서 사용법에 더 익숙해진다면 기존의 nodejs로만 하던 방법보다 더 효율적이고 정돈된 방법이라고 생각했다. 그리고 dto를 설정해서 사용해보았는데 확실히 데이터의 값이 string인지, 아니면 숫자인지를 검증하기 때문에 확실하게 오류를 더 줄여줄 수 있는 방법같다.
문제는 nest에서 postman, insomnia로 데이터 주고받기를 성공했다면 어떻게 클라이언트에서 이를 전달하냐였는데, 이것 때문에 처음에 걱정과 막연한 두려움이 컸다. 하지만 그냥 클라이언트 단에서는 기존의 방식과 같이 axios로 데이터를 입력해주면 되는 간단한 문제였다! (새로운 방식이라고 해서 괜히 미리 겁먹지 말자는 교훈을 얻었다.)

예시:
useEffect(() => {
    axios.get(`${process.env.REACT_APP_SERVER_HOST}/${id}`).then((res) => {
   ...
      const postDataArr = {
        post_id: res.data.post_id,
        title: res.data.title,
        content: res.data.content,
        date: convertDate,
        userId: res.data.userId,
        nickname: res.data.nickname,
      };

      setPost(postDataArr);
    });
  }, []);
  • Multer를 이용하여 사용자 프로필 사진 업로드 및 미리보기 기능
    : multer 또한 그동안 프로젝트에서 사용해 보고 싶었지만 사용해본적이 없던 기능이었기에 먼저 막연한 두려움이 있었다. multer때문에 발표 전 날까지 끙끙댔었던 기억이 난다. 먼저 multer file데이터를 클라이언트에서 전송하는 것부터 오류가 많았다.
    일단 그냥 formdata를 전송하려고 했더니 null, 그냥 빈값이 계속 떠서 결국 따로 데이터 값을 선언하여 클라이언트에서 서버로 전해주었다.
 const formData = new FormData();

      formData.append('id', String(userID));
      formData.append('pw', String(user.pw));
      formData.append('nickname', String(user.nickname));
      formData.append('file', pickedFile);

      const Data = {
        id: formData.get('id'),
        pw: formData.get('pw'),
        nickname: formData.get('nickname'),
        file: formData.get('file'),
      };

      axios
        .patch(
          `${process.env.REACT_APP_SERVER_HOST}/user/upload/${userID}`,
          Data,
          {
            headers: {
              'Content-Type': 'multipart/form-data',
            },
          },
        )
    

그리고 서버단에서 @Body로 데이터를 받아오려니 계속 오류가 나서 @Req를 이용해서 req.body로 데이터를 받아왔다. 또한 파일의 이름의 중복도 방지하고자 따로 파일이름을 설정해주었다. 하지만 upload폴더에 계속 이미지가 생성되는 걸 따로 삭제처리를 해주지 않았기 때문에 이 또한 추후 개선해야할 점이다.

//user.controller.ts

  @Patch('/upload/:id')
  @UseInterceptors(
    FilesInterceptor('file', 1, {
      storage: diskStorage({
        destination: './upload',
        filename: (request, file, callback) => {
          callback(null, `${Date.now()}${extname(file.originalname)}`);
        },
      }),
    }),
  )
  async uploadFile(
    @Param('id') userId: string,
    @Req() req,
  ) {
    await this.userService.uploadImg(req.files[0], req.body, userId);
  }

- 게시판에서 해당 유저의 프로필 사진 보여주기, 미리보기
먼저 get요청에 대한 코드를 미리 만들어줘서 결국 서버에서 get요청을 받아와서 이미지를 미리 보여주는 방식을 이용했다.

  
@Get(':imgpath')
seeUploadedFile(@Param('imgpath') image, @Res() res) {
  return res.sendFile(image, { root: './upload' });
}

이를 통해 파일 고르기에서 선택한 이미지를 미리 보여줄 때도 useState를 이용해서 그냥 이미지를 가져오는 도메인과 이미지가 저장된 파일 이름을 db에서 가져와서 불러오는 방식을 사용했다.

++

추가적으로 기록하고 싶은 부분

  • db에 저장된 시간을 클라이언트에서 보여줄 때 ISOString형식으로 읽혀져서 골칫거리였다. 그래서 결국 함수를 이용해서 localestring으로 변환해주는 방식을 써서, 게시판이나 댓글, 마이페이지에서 보여주었다.
 function formatDate(string) {
    var options = {
      year: 'numeric',
      month: 'numeric',
      day: 'numeric',
    };
    return new Date(string).toLocaleDateString([], options);
  }

  • Entity 외래키 설정
    : 외래키 설정도 애를 많이 먹었다. 결론적으로 manytoone에 joincolumn, 그리고 이의 옵션인 name, referencedColumnName을 설정해줌으로써 무사히 연결할 수 있었다.

    //Comment.entity.ts
    
    @Column({ nullable: false })
    userId: string;
    
    @ManyToOne(() => User, (user) => user.comments)
    @JoinColumn([{ name: 'userId', referencedColumnName: 'id' }])
    user: User;
    //User.entity.ts
    
    @OneToMany(() => Study, (comment) => comment.user, { cascade: true })
    comments: Comment[];

  • app.module.ts 설정
    : 추가적으로 .env를 불러오는 것도 시간이 꽤 많이 걸렸는데, env 파일을 src 바깥 폴더에 저장함으로써 해결할 수 있었다.
    또한 맥과 윈도우의 파일을 읽어오는 방식 때문인지 entities를 [__dirname + '/../**/*.entity.{js,ts}'이렇게 입력하면 metadata를 찾을 수 없다는 오류가 떴었다. 그래서 결국 직접 import하여 입력해주는 방식으로 해결했다.

    @Module({
     imports: [
       ConfigModule.forRoot({
         isGlobal: true,
         envFilePath: '.env',
       }),
       TypeOrmModule.forRoot({
         type: 'mysql',
         host: process.env.DB_HOST,
         port: +process.env.DB_PORT,
         username: process.env.DB_USERNAME,
         password: process.env.DB_PASSWD,
         database: process.env.DB_DATABASE,
         entities: [User, Post, Comment, Study, Chat, Life],
         logging: true,
       }),
       MulterModule.register({
         dest: './upload',
       }),

    그리고 초반에 옵션을 synchronize : true로 해놨기 때문에 mysql 컬럼을 다 지우고 nestjs, tyeporm이 새로 생성하여 metadata를 못읽는다는 에러가 계속 발생했었다. 그래서 synchronize: true를 지워서 해결했고, logging: true로 설정하여 query 상황을 알 수 있어서 개발할 때 편했다.


  • 또한 get 요청을 받을 때 특정 유저의 아이디나 날짜에 데이터를 불러오고 싶을 때와 같이 find 요청도 잘 작동되지 않아 고민이 많았다. 그래서 결국 createQueryBuilder를 사용해서 데이터를 불러왔다.
    또한 이때 where절을 2개, 즉 조건을 2개 걸고 싶다면 .where을 2번 쓰지말고, .andWhere을 쓸 것을 기억하자!
//study.service.ts

  async getStudiesByUserId(userId: string) {
    const TIME_ZONE = 9 * 60 * 60 * 1000; // 9시간

    const koreatime = new Date(Date.now() + TIME_ZONE)
      .toISOString()
      .split('T')[0];

    //userId 일치 & 날짜 일치하는 데이터 불러오기
    const studyDatabyUser = await this.studyRepository
      .createQueryBuilder('s')
      .select(['s.study_id', 's.done', 's.date', 's.content', 's.user_id'])
      .where('s.user_id = :user_id', {
        user_id: String(userId),
      })
      .andWhere('s.date = :date', {
        date: koreatime,
      })
      .getMany();

    return studyDatabyUser;
  }

느낀점

  • 전반적으로 배운 것도 많지만 아쉬움도 남는 프로젝트다. 왜냐하면 nestjs의 배포방식이 기존에 배운 react, nodejs의 방식과 다르다는 점을 고려하지 못해서 배포하는데에 애를 많이 먹었고, 배포 후에 로컬과는 달리 작동하지 않은 기능들이 있었는데 크롤링과 웹 소켓을 이용한 채팅 부분이다. 이는 추후 팀원들과 다시 개선시켜야할 부분이다. 무튼 2주 동안 나 자신한테 너무 수고했다고 말하고 싶고, 팀원들과 다시 열심히 해서 배포를 성공적으로 마무리할테다!

github 링크(혹시 참고하실 분이 계실까봐)

++

nestjs, tyeporm, mysql 데이터 연결에 참고한 유튜브 영상 링크

Anson the developer 이분의 영상을 많이 참고했다!
매우 감사한 분...
https://www.youtube.com/watch?v=W1gvIw0GNl8

profile
📝

0개의 댓글