[F&F 기업협업] 회고록

김광일·2022년 4월 21일
0

PROJECT

목록 보기
5/5
post-thumbnail

길다면 길고, 짧다면 짧은 한달의 위코드 마지막 프로젝트! 기업협업의 끝이 코앞이다.
나에겐 체감상 너무나도 짧고 부족했던 시간이었지만, 배우고 느낀 것은 너무 많다.
이 기억이 그저 아름답게만 미화되고, 사라지기 전에 기록으로 남겨본다.


F&F

내가 가게 된 회사는 1지망으로 지원했던 F&F! MLB와 디스커버리를 다루고 있는 패션의류회사이다.
나의 기대로는 기업에 가면 담당자분이 반갑게 맞이해주고 자리 안내와 함께 화기애애한 시작일 줄 알았다. 하지만 우리에겐 그 누구도 그 어떤 관심도 주지 않았다..

하지만 회고록을 작성하는 이 순간에는 팀장님, 부장님의 상황과 마음을 알기에 전혀 서운치 않다.
아래에서 다시 소개하겠지만 프로젝트의 구성은 기획과 개발이었고, 기획 단계 이후로는 하루도 빠짐없이 야근은 물론 후반엔 주말까지 나와서 열심히 했었는데, 항상 배터지게 저녁도 사주시고, 회식도 두 번이나 하면서 관심이 없는 게 아니셨구나! 하는 결론에 다다르게 되었다. ( 밥 사주는 사람 착한 사람 )


팀구성

팀 구성으로는 프론트엔드 2명과 백엔드 2명이다.

🔥 FE : 강성훈, 김준영
🔥 BE : 김가람휘, 김광일

준영님과는 2차 프로젝트 때 같은 팀이었지만, 준영님의 코로나 이슈로 인해 조금 아쉬움이 남은 상태였는데 같이 오게 되어 잘 됐다는 생각이 들었다.
가람님과 성훈님은 워낙 잘하시는 분들이라 잘 따라갈 수 있을까 조금 걱정이 되기도 했다.

걱정은 이 뿐만이 아니었다. 새로운 환경에 와서 다들 적응하느라 예민할 것인데, 이미 친하게 지내던 사람들과 협업에 오면서, 나도 모르게 장난스레 툭 던진 말들이 저들 마음에 어려움으로 다가가진 않을까 하는 부분이었다.
하지만 결과적으로는 이 부분은 4명이 서로에 대한 이해와 배려로 아무런 문제 없이 지나갈 수 있었다. ( 뻔한 말 같지만 진짜루 )


기획..?

팀장님, 부장님과의 첫 만남 때, 진행할 프로젝트에 대해서 말씀해주셨다.
대시보드, 인플루언서매니징시스템, 등등.. 여러 제안이 있었는데, 결과적으로 선택된 것은 인플루언서매니징시스템이었다. 사내 마케팅직원들이 사용하는 툴인데, 인플루언서와 진행되는 캠페인을 관리하는 툴이었다.
근데 문제가 여기부터였다. 지금까지처럼 클론이 아닌, 기획부터 개발까지 다 우리가 주체적으로 진행을 했어야했다.

처음 진행해보는 기획에 너무나 당황했고, 이게 맞나..? 라는 생각이 들었다.
나는 아무 생각도 떠오르지 않아서 너무 힘들었는데 다른 팀원들은 아이디어가 샘솟았다. 이 부분에 있어 자책도 하고, 팀원들에 대한 미안함도 있었지만, 그렇다고 시작부터 마냥 주저앉아 있을 수는 없다고 생각이 들어 서기를 자처했다.


(처음엔 노션을 사용했는데 용량의 제한이 생겨 중간에 트렐로로 넘어갔다.)

이렇게 회의를 하면서 정리를 하다보니 우리가 하려고 하는 것이 무엇인지 팀원들이 하고자 하는 말은 무엇인지, 어떤 부분이 오류인지에 대한 생각들이 조금씩 명확해졌던 것 같다. 이에 따라 시작은 느렸지만, 기획 초중반부터는 많은 의견도 내고 팀원들과 그 의견을 조율하는 시간을 가졌다.
~ 그리고 우리의 의견은 하나로 모여 행복하게 끝이 났어요~~ 하고 뻔한 해피엔딩으로 끝이 났다면 얼마나 좋았을까? 현실은 그렇게 호락호락하지 않았다.

우리는 야근에, 주말까지 헌납하며 총 9일 동안 기획을 하며, 총 3번의 컨펌을 받았고, 받을 때마다 자신감은 넘쳤지만 결과적으로는 늘 기획이 새롭게 뒤집혔다. 우리가 기획하는 입장에서는 당연하다고 여겨질 것들이 사용자로 하여금 혼란을 야기할 수 있다는 사실이 지금 생각하면 당연하지만 그 당시엔 꽤나 충격이었다.
다들 지쳐갈 즈음 부장님께서 툭 던져주신 아이디어를 develope 시켜 나온 결과! 그것은 바로 본 캠페인이 진행되기 전, 사전캠페인을 의미하는... 이름하야 Pre-Campaign !
이는 프리캠페인이 진행되면, 참여하고자 하는 캠페인의 주제와 맞는 지원사진을 받아서 사내의 기준으로 총평을 점수로 평가하고, 합/불을 결정하는 툴이다.

팀장님이 계속 하시던 말씀이 있었다. 당신들이 하고자 하는 것을 한마디로 정의해보세요. 분명 머리 속에는 우리가 하고자 하는 것이 어떤 건지 다 아는데 막상 입 밖으로 내려니 쉽게 정의되지 않고 주절주절 말이 많아졌다.

그렇게 다듬고 다듬어 정해진 우리의 프리캠페인 툴에 관한 한마디 총 정리!!

“마케팅팀 직원들이 프리캠페인 신청자들을 평가하여
본 캠페인에 참여할 수 있는 인플루언서를 발굴하는 앱”


드디어 개발 시작!

기획 마지막 발표 때, 부장님의 권유로 장고가 아닌 노드와 타입스크립트를 사용하여 프론트와 언어를 통일하게 되었다.
이 때까지만 해도 길고 긴 블라커가 있을 것이라고는 생각하지 못하였다..
그저 행복했던 기획의 단계가 끝이 나고, 꽃 길만 걸을 줄 알았던 우리..
잊고 있었다.. 꽃 길은 비포장이었음을..
하지만 힘든만큼 아름답지 않은가? 결과적으로 지금 되돌아보면 꽤나 아름다웠던 것 같다.

자바스크립트가 처음인지라 타입스크립트에 대한 이해도는 당연 제로였다.
타입스크립트로 진행을 하다가 생각보다 더 어려워서 일단 js로 먼저 진행을 하기로 했지만, 시간이 넉넉하지 않았던 터라 마냥 학습을 하기엔 무리가 있어 보였고, 일단 레퍼런스를 찾아보면서 모르는 부분은 따로 구글링을 하는 것이 효율적이라 판단되었다. node.js + express + sequelize 를 사용하였고, 생각보다 수월했던 것 같다. 아. 직. 까. 진.


팀? 하나!


위코드에서는 보통 백엔드가 ERD를 작성하고 프론트에게 이해를 시켜주는 방식으로 진행이 되었었는데, 이번 프로젝트에서는 테이블 구성부터 컬럼까지 모두 프론트와 함께 작성하였다. 기획부터 ERD 작성까지 프론트와 백의 구분 없이 함께 하다보니 프론트에서 모델에 대한 이해도가 훨씬 높아져 있었고, 다 알고 있는 내용으로 얘기를 하다보니 자연스레 소통하기가 훨씬 수월했던 것 같다.

이를 통해서 하나 더 느낀 것은, 지금까지의 과정을 비하하려는 의도는 아니지만, 지금까지는 개개인이 작업하는 느낌이 컸다면, 이번엔 좀 더 팀이란 하나다! 라는 사실에 대해서 체감을 하게 됐던 것 같다.


초대한 적 없는 불청객, Blocker..

진행하던 중 개발의 과정에서 우리의 발목을 가장 크게 잡았던 것은 로그인 통신과, 모델링 부분이었다.

1. 로그인 통신

<문제점>
1. HTTPS 로 요청이 가게 되는 상황

  • 설정 시 문제가 있었던 것으로 판단, 프론트 측에서 해결
  1. 프론트에서 이메일과 비밀번호를 입력하고 요청을 보내면 백엔드에서 받아오는 Request 값은 [].. 빈 값.. 따라서 email = undefined..
    이에 서로의 입장이 갈리는 상황 ( 다툼은 아니고 조율해가는 과정 )

<2번에 대한 서로의 입장>

  • 백엔드 입장 : 포스트맨에서 요청을 보냈을 때 이상 없이 통신되는 것을 보면 로직상 문제는 없다.
  • 프론트 입장 : 로직상 문제는 없고, 디버깅 과정 중간중간 콘솔을 찍었을 때, body에 들어가는 값이나, 로그인 버튼을 눌렀을 때 보내지는 값을 보면 잘 담겨서 보내진다.

상황만 놓고 보면 어느 한 쪽이 거짓말을 하는 것이 분명하다!

하지만 어느 쪽도 거짓말을 하지 않았다. 이것이 진실.
그렇다면 문제가 무엇이었을까?

// server.ts
import express from "express";
import cors from "cors";
import bodyparser from "body-parser";
import routes from "./app/routes/index";
import config from "./app/config/config";

const app = express();

app.use(cors());
app.use(express.json()); // (1)
app.use(express.json({type: "*/*"})); // (2)
app.use(routes);
app.use(bodyparser.json()); // (3)

app.get("/", (req, res) => {
  res.json({ message: "Welcome Precampaign!" });
});

const PORT:number = parseInt(config.PORT as string);
app.listen(PORT, async () => {
  console.log(`Server is running on port ${PORT}`);
});
  • 몰랐는데 일단 public API 의 경우엔 보통 cors 와 express 두개면 충분하다고 한다.
  • (1)번과 (3)번은 같은 라이브러리이기 때문에 두 번 stringfy가 될 수 있으므로 주의하도록 하자.
  • 결국 해결책은 (2)번이었다. 이 이유를 보아하니 fetch는 axios로 보내게 되면 자동으로 content-type을 추가해주는데 그렇지 않은 경우엔 직접 추가를 해주어야 하는 것이었다. 해서 백엔드에서 지정해준 type:"*/*"의 의미가 모든 타입을 다 받아서 자체적으로 해석해서 사용한다는 것 같다.

2. 모델링..

이렇게 해결이 되고나니 조금은 수월했던 것 같다.
모델링도 잘 되어가는 듯 했고, 리스트 API 작업도 잘 풀려가는 듯 하였다.
참조를 해야하는 상황이 오기 전까진...
sequelize와 associate 에 대한 정의를 제대로 잡지 않은 상태로 진행하다보니 다른 테이블을 참조해오는 데에 많은 어려움이 있었고, 이로 인해 리스트 작업부터 시작해서 그 뒤로 진행되는 모든 일들이 조금씩 밀리기 시작했다. 무한 구글링이 시작되었고, 검색창은 30개를 훌쩍 넘어가서 내가 무엇을 찾았는지 다시 찾아오기도 힘든 지경..
(급 사담이지만, 이런 과정이 있었기에 크롬에서 제공하는 그룹으로 탭 묶기와 북마크 폴더관리에 대해서 알게 되었고, 정리를 하는 방법도 좀 익혔다 ^-^ 결로적으로는 이득)

본론으로 돌아오면, 모델링 작성과 수정의 무한반복이었다.
해치웠나? 스스스스.. 다시 살아나는 모델링..
결국엔 맞는 방법일지는 모르겠지만 해결을 한 방법으론느 association을 따로 지정해주고 as에서 그 값을 받아오는 방식이었다.


소통이 뭔데!

내가 위코드에 오기 전에 알던 소통이라함은, 남들의 이야기를 잘 들어주고, 공감해주는 것이었다.

멍청하였지.. 하지만 위코드에서 프로젝트를 진행하며 배운 소통은 달랐다. 프론트와 백이 서로 키 값을 맞추고 어떻게 요청을 보내고 어떻게 응답을 할 것인지 맞춰보는 것이었다.

멍청하였지... 하지만 기업협업 프로젝트에서 배운 소통은 또 달랐다. 아니 엄밀하게 말하자면 아예 다르다기 보다는 업그레이드라고 해야할까? 키 값을 맞추는 것은 물론이거니와, 프론트와 백이 서로의 로직을 이해하고 있어야하고, 그에 따른 router를 맞추고, 서로 내뱉는 언어 또한 맞춰가는 것이 소통이라는 것을 깨달았다.
이런 사항을 느낀 부분은 많지만 그 중 가장 크게 다가왔던 것을 뽑아보자면,

나는 list를 뽑아내기 위해서 params 값과, body 값이 필요했다.
프론트에게 요구할 때, 캠페인의 params 값과, body로 status 값이 필요하다고 말을 하였고, 프론트에서는 이를 params 값을 떼서 바디로 보내달라는 의미로 받아들였던 것이었다. 물론 코드를 바로 보면서 오해를 풀고 어떻게 말해야하는지 배웠지만, 다른 프로젝트였으면 이런 것을 배울 수 있었을까? 싶기도 하다.


My Part.

캠페인 별로 신청자 리스트 확인

  • 작성을 하고 보니 리팩토링이 정말 간절했다.. 하지만 일단은 정해진 기간 내에 아웃풋을 뽑아야하기 때문에 일단은.. 진행시켜!
const campaignApplicantfindAll = (req: Request, res: Response) => {
    const id = req.params.id;

    Applicant.findAll({
        include: [
            {
                model: CampaignApplicant,
                as: "applicant_campaigns",
                attributes: ["id"],
                where: {
                    campaign_id : id
                },
                include : [
                    {
                        model: Rate,
                        as: "applicant_rate",
                        attributes: [
                            [sequelize.literal(`(SELECT (ROUND(SUM(trend_rate + background_rate + creativity_rate)/ (COUNT(user_id) * 3), 1)) AS rate_avg FROM rates WHERE rates.campaign_applicant_id = applicant_campaigns.id)`), 'rate_avg']
                        ]
                    },
                    {
                        model: ApplicantPlatform,
                        as: "applicant_platforms",
                        attributes: ["account_name"]
                    },
                    {
                        model: Platform,
                        as: "platforms",
                        attributes: ["name"]
                    },
                ],
            },
            {
                model: Campaign,
                as: "campaigns",
                where: {id : id},
                attributes: []
            },
            {
                model: ApplicantKeyword,
                as: "applicant_keywords",
                attributes: []
            },
            {
                model: Keyword,
                as: "keywords",
                attributes: ["name"]
            }
        ]
    })
    .then(applicants => {
        let data = []
        for (const i in applicants) {
            const keywords = []

            for (const j in applicants[i].keywords) {
                keywords.push(applicants[i].keywords[j].name)
            }

            data.push({
                "id": applicants[i].id || [],
                "name": applicants[i].name || [],
                "gender": applicants[i].gender || [],
                "height": applicants[i].height || [],
                "weight": applicants[i].weight || [],
                "thumbnail": applicants[i].thumbnail_url || [],
                "contact": applicants[i].contact || [],
                "address": applicants[i].address || [],
                "platform": applicants[i].applicant_campaigns[0].platforms[0].name || [],
                "platform_account": applicants[i].applicant_campaigns[0].applicant_platforms[0].account_name || [],
                "campaign_applicant_id" : applicants[i].applicant_campaigns[0].id || [],
                "keywords": keywords,
                "rate" : applicants[i].applicant_campaigns[0].applicant_rate[0] || { "rate_avg": "0" },
            })
        }
  • 끝도 없는 include의 반복과 sequelize라는 ORM을 사용했지만, 정작 구현이 힘들 것 같은 부분에서는 row qurey를 사용한 부분.. 반성합니다.
  • es6에서는 async와 await 라는 좋은 기능이 있음에도 then/ catch로 모든 것을 다 처리한 부분.. 반성합니다.

모든 캠페인의 수락된 사람

  • 위의 내용과 같은 내용이므로 넘어가도록 한다.

지원자 이미지 리스트

  • 위의 내용과 같은 내용이므로 넘어가도록 한다.

신청자 평가

  • backgound_rate, trend_rate, creativity_rate 이 세가지를 기준으로 직원들이 각자 별점으로 평가를 할 수 있다.
const rateCreateOrUpdate = (req: Request, res: Response) => {
  const userId = req.userId as unknown as number;
  const campaignApplicantId = req.body.campaign_applicant_id;
  const background_rate = req.body.background_rate;
  const trend_rate = req.body.trend_rate;
  const creativity_rate = req.body.creativity_rate;
  const Where = {
    user_id : userId,
    campaign_applicant_id : campaignApplicantId
  };
  const createRate = {
    campaign_applicant_id : campaignApplicantId,
    user_id : userId,
    background_rate: background_rate,
    trend_rate: trend_rate,
    creativity_rate: creativity_rate
  };

  
  Rate.findOne({
      where: Where,
  })
  .then(rate => {
    if(!rate) { Rate.create(createRate)
      res.status(201).send({
        "rateAvg": Math.round(((rate.background_rate + rate.trend_rate + rate.creativity_rate) / 3) * 10 ) / 10
      })
    } else {
      rate.update({
        background_rate: background_rate,
        trend_rate: trend_rate,
        creativity_rate: creativity_rate
      })
      res.status(200).send({
        "rateAvg": Math.round(((rate.background_rate + rate.trend_rate + rate.creativity_rate) / 3) * 10 ) / 10
      })
    }
  })
  .catch(err => {
    res.status(500).send({
      messge: err.message
    })
  });
}
  • Average 값을 구하는 데에 어려움이 있어 일종의 하드코딩으로 평균 값을 구현한 부분.. 반성합니다..

캠페인 종료 시 신청자 상태 변경

  • 위의 내용과 같은 내용이므로 넘어가도록 한다.

캠페인 별 수락된 신청자

  • 캠페인 별 신청자 리스트와 같은 내용이므로 넘어가도록 한다.

-스스로 내려보는 한줄평:

전체적으로 효율적으로 코드를 짤 필요가 있어보임.
요청이 왔을 때, 콘솔에 찍히는 쿼리를 보면, 얼마나 말도 안 되는 코드인지 깨닫게 됨.


아몰랑 일단 나 칭찬 먼저! 🥕

아쉬운 점도 물론 굉장히 많지만, 그래도 나는 칭찬을 먹고 자란다! 나는 내가 잘 알기에! 스스로 간단한 칭찬타임을 좀 가져보자면, 끝까지 포기하지 않고 완주한 것, 늘 늦은 시간까지 하며 불안함은 있었지만 불평은 없었다는 것, 과정은 참 아름다웠다는 것, 칭찬해~~


아쉬운 점..😭

당근을 먹었으니 이제 채찍 맞을 차례..

  • 로그인과 모델링에서 아주 많은 시간을 잡아먹은 것이 많이 아쉬움으로 남는다.
    • 물론 그 시간마저도 나에겐 성장의 시간이고, 이를 통해서 배운 사실이 아주 많지만, 절대적인 마감선이 있는 상황에서는 좀 더 빠르게 처리할 수 있는 방법을 생각했어야 했는데 팀에서 끌어안고 있었던 것이 조금 아쉬운 점.
  • 무언가 새로운 공부를 했을 때, 기록을 하지 못했다.
    • 임시저장에 넣어놓기만 했는데, 미루지 말고 하나씩 차근차근 꺼내야겠다.
  • 아쉬운 점은 생각나는대로 하나씨 추가할 예정

최종 느낀 점!

기획을 시작하던 당시에 해도 위의 기획 단계에서 언급한 것처럼 이게 맞나..? 싶은데, 다른 기업 팀들은 바로 개발을 시작했고, 사수도 있고, 관심도 많았기에 꽤나 부러워 했지만, 지금은 그 어느 팀도 부럽지 않고 우리 팀이 짱인 것을 안다. (다른 팀 비하 절대 아님 ㅠ)
완벽하진 않지만 기획을 해봄으로써 개발자가 전체적인 흐름을 깨닫는 것이 얼마나 중요한지 알게 되었고, 단순히 개발을 잘 하는 것과 전체적인 큰 그림을 보는 것은 다른 부분이라는 것도 알게 되었다. 이를 통해 나와 의견이 다른 사람들과 의견을 조율하는 방법과 자세에 대해서도 배웠고, 갇혀있던 생각이 좀 트이는 경험도 했다.
특히 이 프로젝트를 통해서 가장 크게 배운 것은 팀이 얼마나 소중한지, 함께 달린다는 것이 어떤 의미인지, 내가 지금껏 소통을 얼마나 잘못하고 있었는지 깨닫게 된 것이다.
혼자였으면 절대 깨달을 수 없었을. 나에게 너무나 힘이 됐던 든든한 팀원들.
성훈 가람 준영님 다덜 너무 고마워~~~ 진짜 너무 고생 많았다~!!!!

프로젝트 시연 영상
Github - PreCampaign

profile
부족함 없이 공부하자

0개의 댓글