5장 영웅, 선의 그리고 프로페셔널리즘

중요하다고 생각한 내용과 나의 생각

107p

우리는 프로페셔널하지 못했다. 우리가 왜 그 일을 하는지 스스로 묻지 않았다. 고객이 실제로 무엇을 원하는지 이래하려 하지 않았고, 다른 대안을 제시하지도 않았다. 고객을 만날수도 없기는 했지만, 요구사항에 대해서 질문을 하지도, 실행할 수 없다는 이야기도 하지 않았다. 우리는 그냥 주어진 환경을 있는 그대로 두고, 무작정 일을 밀어 붙였다. 그때는 그것이 프로페셔널이라고 생각했다. 우리가 노예처럼 일할 때, 영업, 비즈니스 부서 사람들은 항상 정시에 퇴근해 가족과 시간을 보냈다.

  • 그저 순응하는 것이 아닌 나 자신이 할 수 있는 정도를 파악하고 함께하는 팀원에게 이야기하는 것도 정말 중요하다는 생각이 들었습니다.

114p

개발자들이 일정이 너무 짧다고 이야기를 해도 관리자들이 그냥 흘려 들을 때가 많다. 관리자와 개발자 간에 협상이 되어야 하지만 협상 기술이나 제대로 된 근거 자료가 부족해서 개발자들이 압ㄺ에 그냥 굴복하고 말 때가 부지기수다 개발자들은 논쟁하고 싶디 않아서 또는 긍정적인 태도를 보여주고 싶어서 "최선을 다해 노력하겠다"라고 대답해버린다.

  • 평소에 소통하는 능력을 어느 정도 갖추고 있다고 생각하는데 취업시장에서 이러한 점들을 강점으로 어필하면 좋을 거 같다는 생각이 들었습니다.

116p

항상 '아니오'라고만 얘기하는 것은 프로다운 태도가 아니다. 모든 '아니오'에는 반드시 하나 이상의 대안들이 따라와야 한다.

  • 매우 중요하다고 생각합니다.

118p

무언가 약속은 할 수 없더라도 문제 대응이 어떻게 되고 있는지 진척 상황을 계속 공유해야한다.

 

122p

최대한 이른 시점에 붉은 깃발을 세워서 뭔가 잘못되고 있음을 지적하고 팀과 다른 사람들로 하여금 현실적인 다른 대안을 찾아 나설 수 있도록 해야 한다.

  • 현재 진행하고 있는 밋업 프로젝트에서 애자일 방식으로 프로젝트를 진행하고 있는데 불확실성이 큰 요구사항에 대해 살짝 어려울 거 같다는 이야기는 했지만, 해보겠다고 했는데, 이런 상황에서 문제가 생긴다면 빠르게 전해야겠다고 생각이 들었습니다.

 


6장 동작하는 소프트웨어

중요하다고 생각한 내용과 나의 생각

126p

전통적인 관리자들과 비즈니스 컨설턴트들이 절차와 문서의 중요성을 아무리 강조하고 싶어하더라도 소프트웨어 프로젝트에는 소스 코드 그 자체만큼 중요한 것은 없다. 나머지는 모두 부차적이다.

 

128p 코드는 기계장치라기보다는 유기물이다. 코드는 정원처럼 지속적인 유지보수가 필요하다.

 

130p 나쁜 코드를 다루어야 하는 기업은 경쟁력이 떨어지게 된다.

 

132p

기술적 부채를 줄이는 것은 기존의 더러운 것을 청소하는 것이다. 이미 깨끗한 상태를 더럽혀서는 안된다. 즉 어떤 상황이든 추가 코드로 인해 기술적 부채가 더해져서는 안 된다. 하지만 이 개발자는 그렇게 해도 된다고 생각하고 있었다.

  • 해당 개발자처럼 잘못된 이해와 지식에 확신을 가지는 것에 주의해야 겠다는 생각을 했습니다.

133p

빨리 하는 것과 허술한 것은 다르다. 우리의 결정이 어떤 의미이고 어떤 결과를 가져오는지 우리는 잘 이해하지 못할 때가 많다.

 

137p

테스트하지 않았다면 코드 작성을 완료했다고 할 수 없다. 단위 테스트는 코드가 제대로 구현되었는지 확인하는 가장 좋은 방법이다. 테스트 주도 개발을 접해보지 못한 보통의 개발자들은 주어진 요구사항에 맞춰 동작할 거라고 '기대만 하는' 상태로 코드를 작성하고는 바로 다음 요구사항의 구현에 들어간다. 즉 제대로 된 테스트 없이 코딩을 마무리한다. 기능 구현이 완료되었다고 할 수 있으려면 반드시 테스트까지 되어야 한다.

  • 현재 TDD 방식으로 개발을 진행하고 있는데 생각보다 쉽지 않고, 함께하는 팀원은 어려울 거 같다고 해서 일단 저만 진행하고 있는데 이 글을 읽고, 다음 스프린트에서 요구사항 정의와 레드 단계의 테스트 코드를 작성하여 팀원도 같이 TDD를 할 수 있게 해야겠다는 확신이 생겼습니다.
  • 그리고 필자가 작성한 것처럼 테스트 코드를 작성하는 것까지 기능구현의 단계로 생각하여 개발해야겠다는 생각 또한 들었습니다.

 


7장 기술적 실행 관례

중요하다고 생각한 내용과 나의 생각

152p

어떤 실행 관례를 도입한다고 해서 프로젝트의 성공을 보증하지 않는다. 사람은 우리가 원하는 대로 행동하도록 프로그램할 수 있는 기계가 아니다. 단순히 준수할 실행 관례를 공표했다고 해서 기대하는 결과가 나오지 않는다. 실행 관례는 우리가 매일 같이 습관처럼 해야 하는 것이다.

  • 사람을 프로그램할 수 없다는 이야기에 깊이 공감했고, 그렇기에 실행관례를 부분적으로 하는 것이 아니라 실행관례에 대한 명확한 전략을 정의해 이를 진심으로 받아들이고 내재화해야 하기 위해 노력해야겠다는 생각이 들었습니다. 이번에도 포부있게 애자일 방식으로 해보자! 라고 했지만 이를 더 깊이 이해하지 못한 채 적용하려고 했기에 올바른 애자일 방식으로 개발을 하지 못했다는 생각이 듭니다. 상황이 바쁘더라도 애자일하게 개발을 하기위해 정한 실행 관례를 습관처럼 할 수 있도록 노력하고 더 공부해야겠다는 생각이 들었습니다.

 

153p

기술적 실행 관례들 그 자체를 직접적으로 실행할려고 드는 것은 아무런 의미가 없다. 그렇게 해서는 상대방을 납득시킬 수 없다. 실행 관례의 도입 자체를 관리자나 팀 구성원들에게 설득하려 하지 말고 현재 일하는 방식과 비교해서 그것이 가져올 이익에 집중을 해야한다.

 

155p ~ 156p

테스트 코드는 잘 정리된 요구사항의 역할도 하기 때문에 딱 필요한 만큼만 코딩되도록 유도하여 불필요하게 복잡해지거나 오버 엔지니어링을 하는 것을 줄여준다. 이러한 것들이 바로 비즈니스적인 가치다.

  • 해당 문장을 읽고, 개인 프로젝트에서라도 TDD 방식을 적용해 보자는 생각이 들었습니다. 해당 방식에 빠르게 익숙해지고 추후 팀 프로젝트에서 함께하는 팀원들에게도 알려줄 수 있는 개발자가 되기 위해 노력해야 겠다는 생각이 들었습니다.

 

156p

TDD 이름 자체에 '테스트'가 들어 있기는 있지만 사실 TDD는 설계에 대한 실행 관례다. 테스트가 코딩 방향을 주도하면 복잡한 코드를 작성하는 것 자체가 어려워진다.

 

159p

같은 페어끼리 너무 오래 있으면 효과가 적다. 하루 이틀 단위로 주기적으로 페어를 바꾸는 것이 좋다. 그렇게 하면 전체 시스템에 대한 이해도나 개발자의 스킬이 팀 차원에서 누적되어 올라간다. 더불어 코딩 표준을 정의하고 유지하는 데도 도움이 된다.

 

160p

개발자들은 이해하기 어려운 코드를 만났을 때 그것을 개선해보려 하기 보다는 그대로 내버려둔다. 괜히 수정했다가 코드를 망가뜨릴 수 있기 때문이다. 그래서 작은 워크 어라운드들과 중복 코드들을 만들면서 작업한다. 엉망인 코드가 많을수록 엉망인 코드가 늘어나는 속도도 빨라진다. 이것은 '깨진 유리창 법칙'으로도 알려져 있다. 지저분하고 엉망인 애플리케이션은 개발자들을 느리게 만들고 그로 인해 비즈니스도 느려진다.

  • 저의 뼈를 때리는 문장인 것 같습니다. 이번 프로젝트를 진행하면서도 이해하기 어려운 코드들이 있었는데 그냥 넘겼고, 이번에 상호 피드백을 하면서 수정해야 하는 부분을 많이 발견했습니다. 하지만 이미 진행하고 있고, 저도 시험기간 때문에 개발을 미루어 급하게 구현하다 보니 고치지 못하고 현재 개발을 하고 있습니다..ㅠㅠ 제가 리드인데 같이하는 팀원한테 너무 미안하네요..

레거시 애플리케이션을 대상으로 일을 할 때, 전체 시스템을 한꺼번에 새로 작성하고 싶은 욕구를 조심해야 한다. 이럴 때는 수정되는 부분에 한정해서 리팩토링을 집중하는 것이 더 나은 방법이다.

 

실용주의적인 관념 없이 리팩토링하는 것은 대단히 위험하다. 프로페셔널로서 행동한다는 것은 트레이드오프를 이해한다는 것이다. 전체 시스템을 더 좋게 만들 수는 있겠지만, 그럴 필요 자체가 없을 수 있다. 몇년 동안 바뀐 적인 없는 부분을 리팩토링하는 것은 의미가 없다. 애당초 코드를 수정할 필요가 없다면, 리팩토링해야 할 이유도 없다. 리팩토링은 더 자주 변경되는 부분을 대상으로 시작해야 한다.

 

163p

어떤 실행 관례가 다른 실행 관레보다 더 나은지 알아보는 가장 좋은 방법은 프로젝트에 어떤 가치를 주는지, 피드백 루프가 얼마나 긴지 비교해 보는 것이다. 나머지는 상관이 없다. 만약 검토 중인 실행 관례가 우리에게 어떤 가치를 주는지 판단되지 않는다면 도입을 보류해야 할 수 있다.

 

164p

실행 관례에 대한 도입을 이야기하기 전에, 먼저 우리가 이루려는 것이 무엇인지 논의해야 한다. 소프트웨어 개발/납품 절차 중에서 어떤 부분을 얼마만큼 개선하길 원하는가? 이러한 것이 정의되고 나면 그것을 달성하기 위해 어떤 실행 관례를 도입할지 말할 수 있다.

 


8장 길고 긴 여정

중요하다고 생각한 내용과 나의 생각

170p

다음은 기회를 만들어 내기 위해 할 수 있는 몇 가지 활동들이다.

  • 익숙하고 편한 것에서 벗어나 새로운 것을 공부하고 기술적 지식을 확장한다. 예를 들어 새로운 프로그래밍 언어나 기술들을 배운다.
  • 지역 커뮤니티에 정기적으로 출석하거나 행사에 참여한다.
  • 다른 개발자, 비즈니스맨들과 교류한다.
  • 새롭게 배운 것, 지금 하고 있는 것들에 대해 블로깅한다.
  • 오픈 소스 프로젝트에 참여한다.
  • 프로젝트를 만들고 공개한다.
  • 콘퍼런스에 참석한다.
  • 콘터런스에 연사로 나선다.

각자 다른 꿈과 열망이 있고 완전히 다른 상황에 있지만 커리어에 대한 열망을 실현하려 노력한다는 점에 있어서는 매우 비슷하다. 거쳐가는 모든 직장, 프로젝트들 하나 하나를 투자로 인식하는 것이 가장 중요하다. ... 소프트웨어 장인은 거치는 자리마다 끊임없이 지식과 열정에 몰입 그리고 프로페셔널로서의 태도를 키워나간다. 따라서 다른 형태의 투자로서 우리가 기대하는 보상이 어떠한 것인지 명확히 할 필요가 있다.

  • 위와 같은 소프트웨어 장인으로서의 태도와 프로페셔널을 갖추기 위해 노력해야겠다는 생각이 들었습니다.

 

176p

성공적인 커리어는 공짜로 오지 않는다. 스스로 만들어 나가야 한다. 특정 회사 안에서의 커리어보다 개인으로서 우리 자신의 커리어가 항상 우선해야 한다.

성능 최적화가 필요한 이유

프론트엔드를 위한 다양한 기술들이 나왔고, 이제는 성능 최적화에 초점을 맞추고 있습니다. 여기서 구글은 ‘성능이 저하되면 사용자가 떠나고 매출이 감소한다.’라고 이야기합니다. 즉 성능 최적화를 통해 사용자를 늘리고 매출 또한 증가한다는 것입니다. 구글이 조사한 페이지 로딩 속도와 이탈률 및 전환율의 관계를 살펴보면 확 이해될 것입니다.

  • 페이지 표시 시간 2초 증가 → 사용자 이탈률 32% 증가
  • 페이지 표시 시간 4초 증가 → 사용자 이탈률 90% 증가
  • 페이지 표시 시간 5초 증가 → 사용자 이탈률 106% 증가
  • 페이지 표시 시간 9초 증가 → 사용자 이탈률 123% 증가

더해서 다른 회사들의 사례도 본다면 성능 최적화가 얼마나 중요한지 이해할 수 있습니다.

  • 핀터레스트 로딩 시간 40% 단축 → 검색 유입률과 가입자 수 15% 증가
  • COOK 페이지 로드 시간 850밀리초 감소 → 세션당 페이지 조회 수 10% 증가/ 이탈률 7% 감소

 

이러한 성능 최적화는 어떻게 이루어질까?

성능을 결정하는 요소는 크게 두 가지입니다.

  • 로딩 성능

서버에 있는 웹 체이지와 웹 페이지에 필요한 기타 리소스를 다운로드 할 떄의 성능

개선하는 방법 :

  1. 다룬로드해야하는 리소스 수를 줄이거나 크기를 줄이기
  2. 코드를 분할하여 다운로드 하기
  3. 리소스의 우선순위를 매겨 중요한 리소스를 먼저 다운 받기
  • 렌더링 성능

다운로드한 리소스를 가지고 화면을 그릴 때의 성능이며, 자바스크립트 코드에 크게 영향을 받음

개선하는 방법 :

매우 다양하며 서비스 유형에 따라서도 다릅니다. 그렇기에 자신의 서비스에 필요한 최적화 기법을 적용하기 위해선 브라우저의 동작 원리와 사용하는 프레임워크의 라이프 사이클 등 웹 개발의 기본 지식을 이해함으로써 효율적인 코드를 작성해야합니다.

정말 많은 최적화 기법들이 존재하지만 지금은 우리가 제작하고 있는 옷늘날씨 서비스에 적절한 최적화 기법 위주로 정리하겠습니다.

그 전에 성능을 분석할 수 있도록 도움을 주는 개발자 도구에 대해 알아봅시다.

 

성능 분석을 위해 사용되는 개발자 도구를 알아보자

  • Network 패널

현재 웹 페이지에서 발생하는 모든 네트워크릐 트래픽을 상세하게 알려주는 도구입니다. 이를 통해서 어떤 리소스가 어느 시점에 로드 되는지, 해당 리소스의 크기 등을 확인할 수 있습니다.

  • Performance 패널

웹 페이지가 로드될 때 실행되는 모든 작업을 보여 줍니다. Network 패널에서 봤던 리소스가 로드되는 타이밍뿐만 아니라 브라우저의 메인 스레드에서 실행되는 JS를 차트 형태로 볼 수 있습니다. 결론적으로 어떤 JS 코드가 느린지 확인할 수 있습니다.

  • LightHouse 패널

웹사이트의 SEO 점수, 접근성 점수, 성능 점수 등을 측정해주고 개선 방향까지 제시해주는 자동화 도구입니다. 성능 관점에서 해당 도구를 사용하기 위해선 알아야하는 지표들이 있습니다.

총 6가지이며 각 지표에 가중치를 적용하여 총점을 알려줍니다.

  • 6가지 지표
    • FCP(First Contentful Paint) | 10%
    페이지가 로드될 때 브라우저가 DOM 콘텐츠의 첫 번째 부분을 렌더링 하는 데 걸리는 시간 지표입니다.
    • SI(Speed Index) | 10%
    페이지 로드 중에 콘텐츠가 시각적으로 표시되는 속도 지표입니다. 최대한 빠르게 콘텐츠를 표시하면 높은 점수를 받을 수 있습니다. 즉, 페이지 로딩에 같은 시간이 걸리더라도 콘텐츠를 더 먼저 띄운 페이지의 SI 점수가 더 높습니다.
    • LCP(Largest Contentful Paint) | 25%
    페이지가 로드될 때, 화면 내 가장 큰 이미지나 텍스트 요소가 렌더링 되기까지 걸리는 시간 지표입니다. 즉, 가장 큰 콘텐츠가 나타나는데 걸리는 시간을 기준으로 볼 수 있습니다.
    • TTI(Time to Interactive) | 10%
    페이지과 상호 작용이 가능한 시점까지 걸리는 시간 측정 지표
    • TBT(Total Blocking Time) | 30%
    페이지가 클릭, 키보드 입력 등의 사용자 입력에 응답하지 않도록 차단된 시간 총합 지표 동작 방해 작업을 포함한 FCP → TTI 사이의 시간 동안 측정
    • CLS(Cumulative Layout Shift) | 15%
    페이지 로드 과정에서 발생하는 예기치 못한 레이아웃 이동 측정 지표
    ※ 레이아웃 이동 : 화면상에서 요소의 위치나 크기가 순간적으로 변하는 것
  • Opportunities 섹션과 Diagnostics 섹션 : 문제점과 해결 방안, 얻을 수 있는 이점 설명
    • Opportunities 섹션 : 페이지를 더욱 빨리 로드할 수 있는데 도움이 되는 제안
    • Diagnostics 섹션 : 로드 속도와는 관계 없지만 성능 향상에 도움이 되는 제안

그리고 라이트하우스를 이용해 성능을 측정할 때는 기본 네트워크 환경이 아니라 네트워크 속도를 제한하여 어느 정도 고정된 네트워크 환경에서 성능을 측정합니다.

데스크탑은 1x 모바일은 4x로 모바일로 검사할 때 네트워크를 더욱 제한합니다.

 

또 맨날 주기적으로 올린다하고 그러지 못했네요...ㅠㅠ 게으른 저 반성합니다. NCP를 사용하시는 분이 많지는 않겠지만, 사용하신다면 도움이 될거 같아서 최근 프로젝트 CI/CD 구축했던 과정을 공유하려고 합니다. 앞으로는 꼭!! 주기적으로 작성할 예정입니다. 사실 블로깅하는 스터디도 하고 있어서 2주에 한 번씩을 포스팅할  예정입니다 잘 부탁드려요 :)


1. Docker 이미지 빌드

먼저 Next.js 프로젝트에 대한 Docker 이미지를 만들어야 합니다.

루트 디렉토리에 Dockerfile을 만들어 도커 이미지를 만드는 코드를 작성해 봅시다.

 

Dockerfile을 만들기 전에 이미지 빌드에 적용한 것들에 대해서 알아야합니다.

  1. multi-stage 빌드
  2. Next.js의 standalone

 

a. multi-stage 빌드

Multi-stage 빌드 방식을 이용해 도커 이미지를 만들 예정입니다. 해당 방식은 이미지를 만드는 과정에서는 필요하지만 최종 컨테이너 이미지에는 필요 없는 환경을 제거하여 이미지를 최적화하는 방식입니다.

이를 위해 stage를 나누어 기반 이미지를 생성합니다.

 

stage는 다음과 같습니다

  • Base stage (base):
    • npm install --production: 프로덕션에서만 필요한 의존성을 설치합니다. (예: react, next 등)
    • 이 스테이지에서는 프로덕션에서 실행에 필요한 최소한의 패키지만 설치합니다.
  • Dependencies stage (deps):
    • npm install: 개발 의존성까지 포함하여 모든 의존성을 설치합니다. 이 단계는 빌드에 필요한 패키지를 포함하는 것입니다.
    • 이 단계에서 storybook, eslint 등 개발 도구와 관련된 패키지들이 설치됩니다.
  • Build stage (builder):
    • npm run build: Next.js 애플리케이션을 빌드하는 단계입니다.
    • builder는 소스 파일 전체를 복사하고, 빌드를 통해 정적 자원을 생성합니다.
  • Runtime stage (runner):
    • 최종 런타임 이미지로, builder에서 빌드된 결과물과 프로덕션 의존성만 복사해 옵니다.
    • 이 단계에서는 개발 도구나 빌드에 필요한 패키지들이 포함되지 않으며, 실행에 필요한 것들만 포함최소한의 이미지를 생성합니다.

위 과정을 보면 개발 의존성과 프로덕션 의존성을 분리하는데요

두 개념과 왜 분리하는지를 설명하겠습니다.

 

 

1. 프로덕션 의존성 (Production Dependencies)

  • 프로덕션 의존성실제 애플리케이션이 배포되었을 때, 애플리케이션이 정상적으로 동작하는 데 필요한 라이브러리나 패키지입니다.
  • 이 의존성들은 애플리케이션이 실행될 때 필수적입니다.
  • 주로 애플리케이션의 기능 구현실행에 직접적으로 영향을 미치는 패키지입니다.

예시:

  • React, Next.js, Express 등은 프론트엔드/백엔드 애플리케이션의 핵심 라이브러리입니다. 이들은 애플리케이션이 실제로 배포되었을 때 동작하는 데 필요합니다.
  • axios와 같은 HTTP 요청 라이브러리도 프로덕션 의존성입니다.
npm install <패키지명>

# or

npm install <패키지명> --production

 

 

2. 개발 의존성 (Development Dependencies)

  • 개발 의존성은 애플리케이션이 개발 과정에서만 필요하고, 프로덕션 환경에서는 필요하지 않은 패키지입니다.
  • 주로 코드 품질 개선, 테스트, 빌드 도구 등이 여기에 포함됩니다.
  • 개발 중에만 사용되며, 애플리케이션이 배포될 때는 불필요한 패키지입니다.

예시:

  • eslint, prettier와 같은 코드 품질 검사 도구는 개발 시에만 필요하고, 애플리케이션 실행에는 필요하지 않습니다.
  • jest, storybook과 같은 테스트 도구도 개발 중에만 사용됩니다.
  • webpack과 같은 빌드 도구babel도 애플리케이션이 빌드될 때 필요하지만, 배포 후에는 필요하지 않습니다.
npm install <패키지명> --save-dev  # 개발 의존성으로 설치

 

 

왜 프로덕션과 개발 의존성을 구분할까?

  1. 최적화:
    • 개발 의존성까지 모두 배포하면, 애플리케이션의 크기가 커지고, 메모리 및 성능에 영향을 줄 수 있습니다.
    • 프로덕션에서는 실행에 필요한 의존성만 포함시키는 것이 좋습니다.
  2. 보안:
    • 개발 의존성에는 개발 도구테스트 도구가 포함되며, 이는 프로덕션에서 실행될 때 불필요한 취약성을 만들 수 있습니다.
    • 개발에만 필요한 라이브러리를 포함시키지 않으면, 보안 위험을 줄일 수 있습니다.
  3. 간결성:
    • 프로덕션 환경에서는 오직 애플리케이션이 동작하는 데 필요한 최소한의 코드만 포함시켜야 합니다.
    • 개발 과정에서 쓰는 도구는 프로덕션 환경에서는 불필요하므로 간결하고 최적화된 상태로 배포할 수 있습니다.

멀티 스테이지 빌드에서 --production 옵션을 사용하여 프로덕션 의존성만 설치하는 이유는

최종 컨테이너 이미지를 최적화하기 위함입니다.

 

예를 들어:

Dockerfile
코드 복사
# Base stage: Production dependencies
FROM node:18-alpine AS base
WORKDIR /app

# Install only production dependencies
COPY package*.json ./
RUN npm install --production

위와 같이 npm install --production 명령을 사용하면 devDependencies는 설치되지 않고, 프로덕션 의존성만 포함됩니다.

이렇게 하면 최종 컨테이너 이미지는 필수적인 패키지만을 포함하게 되어 이미지 크기와 성능 면에서 최적화됩니다.

 

 

b. Standalone

도커 이미지를 만드는 과정에서 Builder 스테이지에서 구성한 node_modules, package.json 등 필요한 파일을 복사해 와서 이미지에 넣습니다. 이 과정에서 Next.js의 standalone을 사용하면 node_modules의 선택 파일을 포함하여 프로덕션 배포에 필요한 파일만 복사하는 독립 실행형 폴더를 자동으로 생성할 수 있습니다.

 

즉 node_modules를 설치하지 않고도 자체적으로 배포가 가능합니다.

그래서 npm start가 어난 server.js 파일을 대신 사용합니다.

standalone을 설정하기 위해선 next.config.mjs에

module.exports = {
  output: 'standalone',
}

를 추가하면 됩니다.

 

저는 우리가 구성한 프로젝트에 맞게 다음과 같이 설정했습니다.

/next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}

export default nextConfig

 

 

위의 개념들이 적용된 최종 코드는 다음과 같습니다.

# multi-stage 빌드 방식 사용
# 이미지를 만드는 과정에서는 필요하지만 최종 컨테이너 이미지에는 필요 없는 환경을 
# 제거하기 위해 단계를 나누어 기반 이미지를 생성하는 방법

FROM node:18-alpine AS base

# 작업 디렉토리를 설정 -> 도커 컨테이너 안에서 어떤 경로에서 실행할 것인지를 명시
WORKDIR /app

# 프로젝트의 의존성을 복사
COPY package*.json ./

# production 패키지만 base에 설치
RUN npm install --production

# 개발용 패키지를 설치하기 위해 단계 구분
FROM base AS deps

WORKDIR /app

# 패키지 설치
RUN npm install

# Build 스테이지
FROM deps AS builder

WORKDIR /app

# 현재 디렉토리를 /app 디렉토리에 복사
COPY . .

# 프리티어 오류 발생으로 빌드 전에 프리티어 규칙 적용
# 빌드된 파일들에는 프리티어가 적용되어 있지 않아서 미리 적용해 줌
# package.json에 script를 추가하고 "prettier": "prettier --write ." 추가
RUN npx prettier --write .

RUN npm run build

# Runtime 스테이지 : 이미지 생성
# 불필요한 의존성을 가져가지 않기 위해 base를 가져오지 않고, node:18-alpine를 사용
FROM node:18-alpine AS runner

WORKDIR /app

RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs

# next의 standalone을 통해 필요한 파일만 복사하는 도깁 실행형 폴더를 자동 생성
# 아래 설정 next.config.mjs에 설정
# module.exports = {
#   output: 'standalone',
# }
# <https://nextjs.org/docs/pages/api-reference/next-config-js/output>

# standalone을 설정해 줬기 때문에 Build 스테이지에서 /app/.next/standalone가 형성
# 이를 COPY해서 사용 

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT 3000

# 어플리케이션 start
CMD ["node", "server.js"]

# Base stage (base):
# npm install --production: 프로덕션에서만 필요한 의존성을 설치합니다. (예: react, next 등)
# 이 스테이지에서는 프로덕션에서 실행에 필요한 최소한의 패키지만 설치합니다.

# Dependencies stage (deps):   
# npm install: 개발 의존성까지 포함하여 모든 의존성을 설치합니다. 이 단계는 빌드에 필요한 패키지를 포함하는 것입니다.
# 이 단계에서 storybook, eslint 등 개발 도구와 관련된 패키지들이 설치됩니다.

# Build stage (builder):    
# npm run build: Next.js 애플리케이션을 빌드하는 단계입니다.
# builder는 소스 파일 전체를 복사하고, 빌드를 통해 정적 자원을 생성합니다.

# Runtime stage (runner):    
# 최종 런타임 이미지로, builder에서 빌드된 결과물과 프로덕션 의존성만 복사해 옵니다.
# 이 단계에서는 개발 도구나 빌드에 필요한 패키지들이 포함되지 않으며, 실행에 필요한 것들만 포함된 최소한의 이미지를 생성합니다.

위와 같이 Dockerfile을 만들고 .dockerignore 파일을 만들어

이미지 빌드 과정에서 올라가면 안되는 파일 및 폴더를 정해줍니다.

/.dockerignore

node_modules
dist
.env
.git
.dockerignore
logs
*.log
tests
.eslintrc.js
.prettierrc
docker-compose.yml
Dockerfile
README.md

 

이렇게 이미지 빌드에 필요한 파일들을 생성해 주고, 터미널에 다음 명령어를 작성하여 이미지가 빌드 되는 것을 확인합니다.

docker build -t {원하는 이름} .
# docker build -t nextjs-docker . 전 이렇게 작성했습니다.

아래처럼 naming to docker.io/library/nextjs-docker가 나오면 nextjs-docker라는 이미지가 잘 빌드된 것입니다.

 => => exporting layers                                                                                                              0.1s
 => => writing image ~~~~                                        																	 0.0s
 => => naming to docker.io/library/nextjs-docker

 


2. 도커 컴포즈 연동

도커 컴포즈의 역할

  • 여러 컨테이너를 함께 실행:
    • 웹 서버, 데이터베이스, 캐시 서버 등 여러 컨테이너를 함께 실행할 수 있습니다.
  • 컨테이너 간의 네트워크 설정:
    • 컨테이너들 간의 네트워크를 자동으로 설정하여, 서비스들이 서로 통신할 수 있도록 돕습니다.
  • 환경 구성 및 배포의 간소화:
    • 여러 컨테이너의 설정을 하나의 docker-compose.yml 파일에 정의하여, 손쉽게 환경 설정 및 배포를 할 수 있습니다.

즉, Next.js 애플리케이션을 컨테이너화 하여 자동 재시작 및 네트워크 설정 등을 통해 관리할 수 있는 환경을 제공합니다. 확장성을 생각하여 추가했습니다.

/docker-compose.yml

# 도커 컴포즈 -> 여러개의 도커 컨테이너를 듸우기 위해 사용되는 간단한 오케스트레이션 도구
# 참고 : <https://github.com/vercel/next.js/blob/canary/examples/with-docker-compose/docker-compose.prod.yml>

version: '3' # 도커 컴포즈의 버전

services:
  next-app:
    container_name: next-app
    build:
      context: . # Dockerfile이 있는 현재 디렉토리
      dockerfile: Dockerfile
    restart: always
    ports:
      - '3000:3000' # 호스트 머신의 3000번 포트를 도커 컨테이너 내부의 3000번 포트로 연결
    networks: # docker network create my_network로 네트워크를 미리 만들어줘야 한다.
      - my_network

networks:
  # 네트워크 정의를 통해 여러 컨테이너가 서로 통신할 수 있도록 도와주는 기능
  # 각 컨테이너가 같은 네트워크에 속할 때, 컨테이너 이름을 호스트네임으로 사용하여 쉽게 통신할 수 있도록 도와줌
  # 현재 하나의 Container Registry를 사용하기 때문에 필요
  my_network:
    external: true
# 해당 코드를 작성한 뒤
# docker-compose -f docker-compose.yml up -d
# 를 하여 next-app 서비스를 만든다.
# 즉 docker-compose.yml 파일을 통해서 Next.js 애플리케이션을 컨테이너화 하여
# 자동 재시작 및 네트워크 설정 등을 통해 관리할 수 있는 환경을 제공합니다.

 


3. NCP와 연동하여 CI/CD 구축

NCP에서 도커 컨테이너 이미지를 간편하게 저장, 관리, 배포할 수 있는 서비스 Container Registry를 제공해 주는데 이를 이용할 예정입니다.

참고 자료 :

 

NCP Container Registry를 활용하여 CI·CD 환경 구축하기

## 이 글에서 하는 것들 (요약) - NCP Container Registry 생성 - GitHub action 다루기 - Docker image를 빌드하여 NCP...

sungbin.dev

 

위의 링크를 참고해 CI/CD를 구축하겠습니다.

 

 

우선 Container Registry와 연동이 잘되는지 테스트부터 해봅시다.

위의 코드를 입력하고 Container Registry를 생성하고 얻는  Username과 Password를 입력하면 됩니다.

 

이미지 빌드 후 push하기

docker build -t /<target_image[:tag]> -f ./Dockerfile .
# docker build -t [cnergy-registry.kr.ncr.ntruss.com](<http://cnergy-registry.kr.ncr.ntruss.com/>)/cnergy-frontend -f ./Dockerfile . 
</target_image[:tag]>
docker push <CONTAINER_REGISTRY_URL>/<TARGET_IMAGE[:TAG]>
# docker push cnergy-registry.kr.ncr.ntruss.com/cnergy-frontend

해당 과정을 CI/CD를 통해서 main에 push 이벤트가 일어났을 때 실행합니다.

 

 

CI/CD deploy.yml 파일 만들기

루트 폴더에 .github 폴더를 만들고, 내부에 workflows 파일을 만들어 deploy.yml 파일을 생성합니다.

그리고 다음 코드를 작성합니다.

각 코드에 대한 내용은 주석과 name을 읽으면 쉽게 이해할 수 있습니다.

push_to_registry와 pull_from_registry를 구분하여 작업 모듈화

 

push_to_registry

name: deploy next to ncloud

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  push_to_registry:
    name: Push to mcp container registry
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 
        # GitHub Action은 해당 프로젝트가 만들어진 환경에서 checkout하고 나서 실행
        uses: actions/checkout@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to NCP Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.NCP_CONTAINER_REGISTRY }}
          username: ${{ secrets.NCP_ACCESS_KEY }}
          password: ${{ secrets.NCP_SECRET_ACCESS_KEY }}
      - name: build and push
        uses: docker/build-push-action@v3
        with:
          context: .
          file: Dockerfile
          push: true
          tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-frontend:S{{ github.sha }} 
          #깃허브 커밋 해시값으로 이미지 구분

 

pull_from_registry

  pull_from_registry:
    name: NCP 접속 후 이미지 다운로드 및 배포
    needs: push_to_registry
    runs-on: ubuntu-latest
    steps:
      - name: ssh 연결
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.NCP_HOST }}
          username: ${{ secrets.NCP_USERNAME }}
          password: ${{ secrets.NCP_PASSWORD }}
          port: ${{ secrets.NCP_PORT }}
          script: |
            docker pull ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-frontend:${{ github.sha }} 
            docker stop cnergy-frontend || true
            docker rm cnergy-frontend || true
            docker run -d -p 80:3000 --name cnergy-frontend ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-frontend:${{ github.sha }}
            docker image prune -f

 

 

GITHUB_SHA 환경 변수를 사용한 이미지 선택

 

GITHUB_SHA 환경 변수를 이용해 특정 커밋에 해당하는 이미지를 가져오거나, 만약 설정되지 않았을 때 최신 이미지를 가져오는 방식입니다. 이전 코드에서는 항상 커밋 해시값을 사용하고 있는데, 이와 같이환경 변수를 체크하여, 필요시 최신 이미지를 사용할 수 있는 옵션을 추가할 수 있습니다.

 

최종코드입니다.

# 작업 모듈화
name: deploy next to ncloud

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  push_to_registry:
    name: Push to mcp container registry
    runs-on: ubuntu-latest
    steps:
      - name: Checkout # GitHub Action은 해당 프로젝트가 만들어진 환경에서 checkout하고 나서 실행
        uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: NCP 레지스트리 로그인
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.NCP_CONTAINER_REGISTRY }}
          username: ${{ secrets.NCP_ACCESS_KEY }}
          password: ${{ secrets.NCP_SECRET_ACCESS_KEY }}

      - name: 도커 이미지 빌드 후 푸시
        uses: docker/build-push-action@v3
        with:
          context: .
          file: Dockerfile
          push: true
          tags: ${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-frontend:S{{ github.sha }} #깃허브 커밋 해시값으로 이미지 구분

  pull_from_registry:
    name: NCP 접속 후 이미지 다운로드 및 배포
    needs: push_to_registry
    runs-on: ubuntu-latest
    steps:
      - name: ssh 연결
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.NCP_HOST }}
          username: ${{ secrets.NCP_USERNAME }}
          password: ${{ secrets.NCP_PASSWORD }}
          port: ${{ secrets.NCP_PORT }}
          script: |
            if [-z "$GITHUB_SHA" ]; then
              echo "GITHUB_SHA 환경 변수가 설정되지 않았습니다. 최신 이미지를 사용합니다."
              IMAGE_ID=$(docker images --format "{{.ID}}" | head -n 1)
            else
              echo "GITHUB_SHA: $GITHUB_SHA 이미지를 사용합니다."
              IMAGE_ID=${{ secrets.NCP_CONTAINER_REGISTRY }}/cnergy-frontend:${{ github.sha }}
            fi

            docker stop cnergy-frontend || true
            docker rm cnergy-frontend || true
            docker run -d -p 80:3000 --name cnergy-frontend $IMAGE_ID
            docker image prune -f

 


4. 서버에 도커 설치

서버 접속

윈도우 → cmd (관리자 권한으로 실행)

맥 → 터미널에서 sudo로 실행

 

필요 패키지 설치 및 키 등록

sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL <https://download.docker.com/linux/ubuntu/gpg> -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \\
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] <https://download.docker.com/linux/ubuntu> \\
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \\
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  
  
sudo apt-get update

 

 

도커 설치

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin

stand-alone 방식으로 설정해서 도커 컴포즈 플러그인은 설치 x

 

도커 잘 설치되었는지 확인

sudo docker run hello-world

 

 

이렇게 진행한 후 깃허브 액션에 시크릿 키들을 설정하면 끝입니다!

긴 글 읽어주셔서 감사합니다 :)

프로젝트를 진행하면서 빠르게 버그를 고치거나 예상치 못한 상황에 대한 작업을 하면서 불필요한 커밋이 생기는 경우가 많습니다. 이럴 경우 커밋으로 기존 작업들을 파악하는 데 어려움이 생기는데요 커밋을 가독성 있게 보고자 Git의 Interactive Rebase 기능과 Squash Merge를 이용해  불필요한 커밋들을 합쳐보겠습니다! 맨 마지막에 요약을 해 놓았으니 빠르게 하셔야 하는 분들은 요약을 보면 좋을거 같습니다 :)

 


1. 원하는 커밋의 기준점 찾기

git log --oneline

위의 명령어를 입력하여 커밋 히스토리를 확인하고, 합치고자 하는 커밋들이 어느 지점부터 시작되는지 그리고 어디서 끝나는지 확인합니다.

 

제가 위 명령어를 입력하면 아래와 같이 나옵니다.

 

전 HOTFIX가 정말 많아서 깃허브에서 각 커밋 히스토리를 보면서 각 HOTFIX를 분류했습니다. 부끄럽네요..ㅠ

 

2. 합치고 싶은 커밋을 rebase 하기

합치고 싶은 목록의 시작 커밋 이전 커밋을 선택합니다.

git rebase -i <base-commit-hash>

 

제가 수정하고 싶은 부분은

해당 부분에서 f34a762 - 4830ae0 커밋을 합치고 싶습니다.

그래서 그 이전 커밋인 e0c7bf8를 입력합니다.

git rebase -i e0c7bf8

 그럼 위와 같이 뜨는데 합칠 커밋들의 pick을 squash로 바꾸어주고 :wq를 입력해 저장하면 됩니다. 저는 vscode에 새로운 파일 창일 열렸는데 이 경우에는 그 창에서 수정하고 Ctrl + S로 저장한 후 파일을 닫으면 됩니다. 자세한 내용은 아래 설명했습니다.

 


 

저같은 경우는 해당 과정에서 3가지 문제를 맞닥뜨렸는데 아래와 같이 해결했습니다.


1. 터미널에 파일이 뜨는 것이 아니라 새로운 파일이 생성됨

 

제가 vs code에서 git rebase -i e0c7bf8 명령어를 입력했을 때는

 위와 같은 파일이 생성되어 당황했었습니다.

 

이럴 경우 switch to text 버튼을 누르고 pick을 s로 바꿔주면 됩니다.

 

한 가지 방법이 더 있는데 터미널에서 git config --global --list를 입력해 설정을 확인해주고, 기본적인 설정

core.autocrlf=true
user.name=0000
user.email=0000@gmail.com
init.defaultbranch=main

이렇게가 안되어 있다면,

 

git config --global --unset core.editor를 입력하여 기본 설정으로 바꾸면 vscode를 이용하지 않고 터미널 내에서 일반적인방법으로 수정이 가능합니다.


2. 합칠려는 커밋 중간에 merge된 브랜치가 있어 관련없는 커밋이 중간에 낌

 

git log --oneline으로 확인했을 때는 5efec9f 커밋 한 줄만 있어서 몰랐는데

리베이스를 하려고 보니까 5efec9f 커밋이 feat/#1_온보딩프로세스구현 브랜치를 merge한 커밋이기 때문에 해당 브랜치의 커밋들이 중간에 생기는데요

 

이럴 경우에는 다음과 같이 합치려고 했던 커밋의 시작 커밋 바로 아래에 s를 입력한 커밋들을 입력해야 합니다. 

여기서 pick은 '있는 그대로 새 히스토리에 적용하겠다.'라는 의미이고, s(squash)는 '직전(혹은 이전에 pick된) 커밋에 합치되, 메시지도 합쳐서 수정하겠다.'라는 의미입니다.

 

그렇기에 s가 입력된 커밋을 합치려고 했던

pick f34a762 Update deploy.yml

 커밋 아래로 배치해야 합니다.

 

여기서 중요한 점은 사이에 있는 커밋들이 s를 입력한 커밋과 연관되어 있지 않은 경우에만 순서 재배치가 가능합니다. 제가 수정할려고 하는 커밋들은 ci/cd 배포 코드이기에 중간에 있는 커밋들과 연관되어 있지 않습니다. 그래서

이렇게 수정했습니다. 이후 Ctrl + S를 눌러 주고 파일 창을 닫으면 적용됩니다.


3. 수정 중에 실수로 파일창을 닫으며 원치 않는 상태에서 작업이 중단됨

 

이런 경우에는

git rebase --abort

 

를 입력하여 리베이스를 취소하면 됩니다.


 

3. 커밋 이름 새로 짓기

위의 작업을 실행하면 아래와 같은 화면이 뜹니다.

작성된 커밋들을 지우고 원하는 커밋명을 작성해주면 됩니다.

 

작성 후 2번 작업과 동일하게 :wq를 눌러 저장해 줍니다.

입력할 때는 i를 눌러 입력하고 esc를 눌러 입력을 완료하는 건 모두 아시죠?

 

4. 충돌 해결

3번 작업이 끝나면 리베이스를 수행하는 과정에서 conflict가 발생하는데 이를 해결하고 git rebase --continue를 해줘야 합니다.

 

git status 또는 IDE에서 제공하는 기능을 통해 conflict를 해결할 수 있습니다.

 

충돌을 해결한 후

git add .
git commit --amend
git rebase --continue

 

명령어를 입력하면 됩니다.

 

아마 충돌이 계속 발생할 가능성이 큽니다. 그럴 때마다 충돌을 해결하고 위 명령어들을 입력하는 것을 반복해 리베이스가 완료될 때까지 기다립니다.

 

부가적으로 설명 드리자면 git commit --amend는 이전 커밋에 덮어쓰는 것입니다.

 

5. git 강제 푸시

리베이스를 모두 완료한 후에

git log --oneline

을 입력해 제대로 적용되었는지 확인하고,

 

git push --force  # 또는 --force-with-lease

를 입력해 강제 푸시를 해주고, main 브랜치와 merge를 해주면 됩니다.

 


요약

  1. git log --oneline으로 합치려고 하는 커밋 범위 확인
  2. git rebase -i  {합치려고 하는 커밋들의 시작 커밋의 이전 커밋의 해시값 입력}
  3. 스쿼시할 커밋을 pick에서 s로 변경 -> :wq를 입력해 저장
  4. 커밋 이름을 정하는 파일이 뜰 텐데 이전 커밋 이름을 제거하고 원하는 커밋으로 변경 후 :wq
  5. conflict가 발생할 텐데 충돌해결 -> git add . -> git commit --amend -> git rebase --continue를 반복하여 리베이스를 마무리 해주기
  6. git log --oneline으로 적용된 것을 확인하고, git push --force로 강제 푸시

 

오늘도 글 읽어주셔서 감사합니다. 너무 오랜만에 올려서 죄송하기도 하네요ㅠㅠ 앞으로 1주일에 1개씩 업로드 할 예정입니다. 해결하지 못한 문제나 궁금한 것이 있다면 댓글 남겨주세요 제가 해결했던 문제면 포스팅 하겠습니다 :)

+ Recent posts