문제 해결 - 1차 플젝

Sinf·2022년 5월 18일
1

고민의 흔적

목록 보기
21/38

문제 해결

프로젝트를 진행하면서 생긴 문제와 해결에 대해서 글로 정리한 것이 없어 노션에 저장된 기록들을 하나의 글로 정리하려고 한다.

이미지 처리

문제 상황

Post를 생성하는 단계에서 multer 라이브러리를 사용해 이미지 파일을 업로드한다. Post가 등록되지 않을 때(이미 등록된 title이거나 빈 문자열 등등), 업로드된 이미지에 대한 처리가 문제가 되었다.

문제 해결

  1. Post가 등록되지 않는 경우, 이미지 파일을 삭제하는 로직을 생성한다.
  2. 가능하다면 미들웨어로 등록된 multer를 뒤로 넘겨서 Post가 등록될 경우 이미지 파일을 저장한다.

1번의 경우, 이미지 파일에 대한 정보가 미들웨어에서 모두 처리되고 넘어오기 때문에, 삭제할 때도 어렵지 않았다. 1개의 서버를 사용하기 때문에 경로만 알면 삭제가 가능했다.

2번의 선택을 할 경우, 검증을 통과하더라도 DB에 저장할 때 제대로 저장이 되지 않는 경우 삭제 로직이 필요했다.

처음엔 이미지 삭제 없이 로직을 구현할 수 있다고 생각했는데, 진행할수록 삭제 로직은 필요했기 때문에 1번의 방법을 선택하게 되었다.

CommonJS vs ES module

라이브러리를 가져올 때, CommonJS을 사용하다가 ES module을 사용할 때, 그냥 사용하면 에러가 발생한다.

package.json에서 "type": "module"을 추가해야 했다.

목록 호출 API

문제 상황

이거 먹어봄?에서는 지역 리스트 - 지역 내 Post로 연결된다.
지역 리스트를 구성하는 방법에 대해 고민이 있었다.

  1. Post가 있는 지역만 보여준다.
  2. 지역 리스트에서도 지역 내 Post에 저장된 이미지를 가져와야 한다.

문제 해결

전체 Post를 가져와 파싱하기

const posts = await Post.find();

return posts.reduce((prev, post) => {
  if (prev[post.wideAddr]) {
    return prev;
  }
  prev[post.wideAddr] = post;
  return prev;
}, {})

해당 로직은 Post 전체를 탐색하면서, 주소 값을 Key 값으로 사용한다. 이미 대표 Post가 등록된 경우는 지나간다.

지역 리스트를 호출할 때마다, 전체 Post를 호출해야 하나?
그리고 reduce로 Post 전체를 탐색하는 로직이 좋은가?

Location 도메인 분리하기

생각해봤을 때 Post 전체를 가져오는 것은 문제가 아니지만, reduce로 Post 전체를 탐색하는 것은 문제가 된다고 생각했다.

Post에서 주소를 관리하는 것 말고, Location에서 주소를 관리하는 것은 어떨까 생각했다. (도메인 분리) Location에서 주소를 관리하고, posts 배열로 Post를 가져오는 것이다.

const locations = await Location.find({
  posts: { $type: "array" },
}).populate("posts");

Post 전체를 탐색하지 않고, Location을 가져와 지역 리스트를 구성할 수 있었다.

대신, Post를 저장할 때 Location에 post를 추가하는 로직이 필요했다.

const post = new Post(
	// Post 정보
);

await Location.updateOne(
  { wideAddr, localAddr },
  {
    $push: {
      posts: post, // posts 배열에 post 추가
    },
  },
);

리팩터링

문제 상황

해당 코드를 다시 구현하면서, 모델 스키마의 변경, 내부 구현 로직의 변경 등 리팩터링 요소가 많았다. 리팩터링이 진행되면서 잘 구현되었는지, 문제가 없는지 확인하기 위해 계속 서버를 실행하고 API 요청을 보내고 확인하는 상황이 반복되었다.

빠르게 서비스를 제작해야 하는 입장에서 시간 문제가 계속되었다.

문제 해결

테스트 코드 작성

해당 API의 로직을 검증할 수 있는 테스트 코드를 작성하기 시작했다.

Jest, Supertest를 이용해 요청에 따른 응답을 검증하는 방식으로 테스트 코드를 작성했다.

nodemon과 같은 Dev 환경에서 서버 실행

nodemon과 같이 Dev 환경에서 코드가 변경될 경우 서버가 자동으로 재시작되는 도구를 사용하기 시작했다.

추가로 package.json에서 script에 npm 명령어를 등록해 사용했다.

테스트 코드 작성

문제 상황

Supertest를 진행하면서 실제 DB에 데이터가 저장되었다.
구글에 검색해보니 테스트가 많아지면서 테스트 실행 시간 문제,
브랜치 병합 시 테스트 문제 (CI) 등등 여러 문제가 있었다.

문제 해결

Mocking. DB에 가해지는 코드를 실제로 실행시키는 것이 아니라 가짜 함수로 만들어 진행한다. 실행되는지, 리턴 값을 가짜로 정해두고 그 값이 오는지 등등 테스트를 진행한다.

당장 플젝에 적용하려면 추가적으로 공부가 필요했다.

일단, 적재되는 데이터를 없애기 위해
테스트가 종료되면 DB에 데이터를 삭제하도록 했다.

afterAll(() => {
  // DB 데이터 삭제
}

당장의 임시방편이므로 Mock 테스트가 꼭 필요했다.

테스트 코드 처음 작성하면서 생긴 에러

ReferenceError: regeneratorRuntime is not defined

Supertest로 테스트할 때, async/await을 사용하려면 regenerator-runtime 라이브러리를 테스트 파일에서 가져와야 한다.

app.address is not a function

Supertest를 진행할 때, 요청에 app을 전달해야 한다. app.js에서 appexport하지 않아서 생긴 문제였다.

listen EADDRINUSE: address already in use :::3000

테스트는 병렬적으로 이루어진다. app을 3000 포트에서 실제로 실행하면서 테스트가 진행되어 생긴 문제였다.

if (process.env.NODE_ENV !== 'test') {
  app.listen(3000, () => {});
}

test 환경이 아닐 경우에만 앱이 실제로 실행되도록 했다.

Jest를 통해 앱을 테스트할 땐, 앱 로직을 가져다 사용하지만 실제로 포트에서 실행할 필요는 없다.

Aborted

파일 업로드해 Post를 생성하는 로직에서 발생했다.
파일을 업로드하는 테스트를 작성하는 코드에 문제가 있었다.

it('파일 업로드', async () => {
  const resposne =
        await request(app)
                .post('')
                .field('title', 'title')
                .attach('images', '/path/to/image');
});

파일을 업로드할 땐 attach를 사용한다.

협업의 문제

문제 상황

처음 팀플을 진행하면서 Git 충돌 문제, DB 설정 (DB 이름, 컬렉션 이름, DB 호출 방식) 등 혼자할 때 고려하지 않고 넘어간 문제들이 발생했다.

문제 해결

Git 충돌 문제

덮어두고 Merge 하면 패가망신 못 면한다,

항상 먼저 원격 저장소의 최신 브랜치를 갱신하고, 병합할 브랜치에 Merge하고, 충돌이 있는지 확인한다. 그리고 충돌을 해결하고 병합하도록 한다.

DB 설정

DB는 팀에서 서로 공유되는 코드 부분이기 때문에 서로 의견 공유가 잘 되었어야 한다.

mongoose.connect(/*Mongo DB URI*/);

DB 이름을 어떻게 사용할지, 컬렉션 명은 어떻게 사용할 지, 각각 로컬 환경이 다르다보니 문제가 되었다. URI 자체를 환경 변수로 사용하는 것과 Port, Host 등을 환경 변수로 쪼개서 사용하는 등 서로 사용하는 방식이 달랐다.

팀원 간의 대화가 중요했고, 대화를 통해 정하고 넘어감으로써 문제는 쉽게 해결할 수 있었다.

업로드 파일 Too Large

문제 상황

Post 생성하는 요청에서 실제 서버를 운영할 때 요청의 크기가 너무 커서 요청이 거부 당하는 상황이 발생했다.

문제 해결

Post를 생성할 때, 이미지 파일을 함께 저장하다보니 문제가 생기는 것 같다. (이미지 파일의 용량이 커질수록 문제가 될 듯)

구글에 검색해보니 express에 내장된 bodyParser를 사용하는데, 추가적인 설정이 없으면 요청의 body 값을 100KB로 제한한다.

app.use(express.json({ limit: '50mb' }));

Body에 담길 수 있는 크기를 넉넉하게 늘려줘 업로드할 수 있도록 했다.

추천, 댓글이 있는 포스트 삭제 시 에러

문제 상황

추천이나 댓글이 있는 포스트를 삭제할 경우 내 추천한 목록, 내 댓글 목록을 불러오지 못하는 문제가 발생했다.

문제 해결

테스트 코드에서 에러가 발생하지 않고, 프론트와 연결하는 과정에서 생긴 문제였다.

그래서 먼저 해당 코드를 살펴봤다.

export async function deletePost(postId, authorId) {
  try {
    const isExist = await Post.findById(postId);
    if (isExist.author.toString() !== authorId.toString()) {
      throw new Error("권한이 없습니다.");
    }

    const post = await Post.findByIdAndDelete(postId);
    const { photos, address } = post;
    photos.forEach((photo) => {
      
    //... 이미지 삭제, Location에서 삭제
  } catch (e) {
    throw new Error("존재하지 않는 글입니다.");
  }
}

혼자서 볼 땐 몰랐는데, 팀원들과 함께 로직을 하나씩 살펴본 결과 포스트가 삭제될 때, 북마크와 댓글이 삭제되지 않았다. (서로 참조하는 관계) 그렇다보니 북마크와 댓글을 가져오는 로직해서 에러가 발생하는 것이었다.

export const getCommentsByUserId = async (
  _id, page, perPage,
) => {
  // ... pagination Info

  const comments = await Comment.find({ author })
    .populate("author")
    .populate("post")
    .sort({ createdAt: -1 })
    .skip((page - 1) * perPage)
    .limit(perPage);

  // ... return data
};

Comment를 가져올 때, Post를 합쳐서 가져오는데, 없는 Post를 가져오다보니 Post에서 Null이 발생해서 문제가 됨.

일단, Post를 삭제할 때 Bookmark와 Comment를 삭제하는 로직을 추가해 해결했다.

내가 생각한 테스트 코드로 잡아내지 못하는 문제들이 있었는데, 테스트 코드를 더 잘 짜야겠다. 상황을 잘 이해하고 이에 맞게 테스트 코드를 작성해야겠다.

파일 리사이징 비동기

문제 상황

업로드된 이미지 파일들을 리사이징하는데, 다음과 같은 코드를 사용했다.

export default async function resizeFile(files) {
  files.forEach((file) => {
    const filePath = path.join(process.env.UPLOAD_PATH, file.filename);

    sharp(filePath)
      .resize({ width: 1080 })
      .withMetadata()
      .toBuffer((err1, buffer) => {
        if (err1) throw err1;
        writeFileSync(filePath, buffer);
      });
  });
}

파일들을 리사이징하는 로직을 모든 파일에 forEach로 반복해 진행했는데, 생각해보니 중간에 실패할 경우 실패했는지, 무엇이 실패했는지 확인할 수 없었다. 그저 되었다고 믿는 상황이었다. 또한, forEach에서는 동기로 실행할 수 없었다.

  1. 요청에 따른 응답의 불확실성
  2. 비동기로 실행되어 포스트 등록 시 이미지 호출 문제

문제 해결

먼저 동기로 실행하기 위해 forEach를 사용하지 않고 for를 사용하는 것을 생각했다. 하지만 응답의 불확실성을 해결하기 위해서 배열을 생성하고, 배열에 응답을 저장하는 방법을 사용해야 했다.

Promise.allmap을 사용하는 방법. map으로 파일을 리사이징하는 프로미스 객체를 생성했다.

export default async function resizeFile(files) {
  const result = await Promise.all(
    files.map(async (file) => {
      // ... resize logic
    }),
  );
}

그리고 Promise.all을 사용해 해당 프로미스 객체 배열의 결과를 배열로 받았다. 이를 통해 동기적 실행과 파일 리사이징에 대한 응답의 확실성을 가질 수 있었다.

JS to TS

Express JS를 사용하면서, 자유로운 코드 작성이 가능했지만 타입에 대한 불확실성이 문제가 되었다. (옵션, 함수 파라미터 등등) TS를 사용해 타입 추론, 타입 안정성을 보장하면서 더 빠르고, 안정성을 갖춘 개발을 진행하게 되었다.

profile
주니어 개발자입니다. 🚀

0개의 댓글