구름톤 풀스택 개발자 양성과정 7회차를 수강하며 마지막으로 진행한 파이널 프로젝트입니다.

Web IDE 만들기 프로젝트와 다르게 필수 기능은 없지만, 카카오 API를 최소 1개 사용해야한다는 조건이 있었습니다.

그 외 기획은 자유 주제라서 프로젝트 기획에만 2주의 시간이 소요되었습니다.

우리 팀은 자기 주장이 강하고 각자의 성향이 달라 의견을 취합하기가 힘들었습니다 🥲

이틀 간 아이디어 회의만 10시간 한 끝에, 모두가 "좋다 !" 라고 말했던 아이디어가 나왔고, 해당 아이디어를 구체화하여 프로젝트를 진행할 수 있었습니다.

 

프로젝트 인원은 첫 프로젝트인 Web IDE 프로젝트 팀원에 변동없이 진행하며, 프론트엔드를 한명 더 추가하자는 의견이 있었습니다.

그래서 프로젝트 전 저와 스터디를 함께했던 프론트엔드 1분을 영입하여 총 FE 3명, BE 4명으로 프로젝트를 진행하였습니다.


프로젝트 일정

- 기획 기간 : 2024.06.26 ~ 2024.07.12

- 프로젝트 기간 : 2024.07.08 ~ 2024.08.14

구현하고 싶은 기능이 많은터라, 기획을 서둘러 끝내고 바로 개발을 시작하였습니다.

1차보다 기간이 조금 더 길어지고, 시간에 쫓겨 본 경험이 있는터라 이번에는 타임라인을 확실하게 설정하고 매 주 스프린트를 진행하며 최대한 효율적인 시간내에 프로젝트를 완료할 수 있었습니다.

프로젝트 마감일인 13일보다 4일 빠르게 추가 기능까지 모두 구현할 수 있었습니다. 

이미지는 실제로 저희가 진행했던 타임라인입니다.

제가 맡은 부분은 채팅 기능(노란색)과, 알림 기능(연두색), 리뷰 기능(회색)으로 기획 발표 때 사용할 PPT를 제작하느라 다른 팀원들보다 조금 늦게 개발을 시작했습니다.


프로젝트 개요

 

- 프로젝트 <Dofarming> <도파밍>

- 랜덤 좌표를 이용해 랜덤으로 국내 여행지를 추천, 여행지 주변에 갈만한 장소(식당, 카페, 관광지 등)을 추천해주는 웹 서비스

- 도파밍뇌 신경전달물질인 '도파민게임 용어인 '파밍'의 합성어로, 사람들이 재미와 즐거운 경험을 적극적으로 찾아 나서는 사회 현상

- 상징 색상 체리 레드 : 에너지, 즐거움을 추구하는 강렬한 레드

- 로고 : 여행자의 모자, Farming의 농부를 상징

기획 의도

  • 인스타그램 릴스에서 #무계획여행 검색 시 지도에 매직을 던져 매직이 찍힌 곳으로 여행을 떠나는 챌린지에서 아이디어를 얻음
  • 인스타그램 릴스, 유투브 숏츠에서 도파민을 얻는 것에 무뎌진 청춘들을 위해
  • 여행 계획을 짜는 것에 지쳐 버리는 사람들을 위해
  • 당장 어디론가 떠나고 싶은 사람들을 위해

 

프로젝트 주요 기능

1. 일반 회원 가입/로그인 및 소셜 로그인 (구글, 카카오, 네이버)

2. 로그 서비스 : 추천 받은 랜덤 여행지의 기록 저장 가능

3. 채팅 서비스 : 특정 여행지와 관련된 오픈 채팅 생성 및 정보의 실시간 교류 가능

4. 랜덤 여행지 선정 및 추천 : 랜덤 좌표 선정 및 해당 좌표 부근의 추천 장소 확인 가능

5. 좋아요 & 리뷰 서비스 : 추천 받은 장소(가게, 관광지 등)에 좋아요 및 리뷰/별점 작성 가능

 

완성된 프로젝트

시연 녹화 영상
프로젝트 발표 - 실시간 시연

영상을 보기는 귀찮은 사람들을 위해서 몇가지 기능만 GIF로 붙여볼게요 ( GIF 크기 이슈로 몇개만 첨부 ㅜ )

일반 회원가입
테마 설정
링크 공유 & 카카오톡 공유

기술 스택

Frontend Backend
  • NextJS
  • TypeScript
  • pnpm
  • Styled-Component
  • SCSS(SASS)
  • Recoil
  • Axios
  • React Query
  • Stomp
  • SockJS
  • MSW
  • Spring
  • Java
  • MySQL
  • Spring Security
  • JPA
  • QueryDSL
  • Kafka
  • Gradle
  • H2
  • SwaggerUI
  • Stomp

CI/CD : Git Action, AWS S3, CodeDeploy 사용

http → https 리다이렉션 : 백엔드 - SSL, 프론트엔드 - NgineX 사용


담당 기능

제가 담당한 기능은 아래와 같습니다.

실시간 채팅 및 채팅방 생성 기능

: 사용자가 제목, 지역, 해시태그를 설정하여 채팅방을 생성

  • 채팅방의 이미지는 해당 지역의 로고로 설정
  • 해시태그의 색상은 총 12가지로 랜덤 설정

: 다수 사용자의 실시간 채팅 기능

: 채팅방 입장 퇴장 메세지 뷰

: 채팅방 클릭 시, 안읽은 메세지가 존재하면 안읽은 메세지 중 첫 메세지로 스크롤 이동

실시간 채팅 알림

: 최초 웹 페이지 실행 시 SSE를 통해 알림 서버 연결

: 알림 수신 시 채팅 메뉴 아이콘에 뱃지 표시

: 채팅 메뉴 접속 상태에서 알림 수신 시, 내 채팅 리스트에서 안읽은 메세지 개수 증가

장소 리뷰 CRUD

: 장소에 대하여 평점과 리뷰 작성 및 수정, 삭제 구현

: 리뷰에 좋아요 기능 구현

: 리뷰에 업로드된 이미지 크게 보기 가능

사용자 프로필 수정

: 사용자 프로필 이미지 업로드 및 삭제 가능

일반 회원 : 닉네임, 비밀번호, 이미지 변경 가능

소셜 회원 : 닉네임, 이미지 변경 가능

: 현재 로그인 된 계정이 일반인지 소셜인지 확인 가능


프로젝트 소통 및 진행 방식

1. 데일리 스크럼 및 매일 회의

교육 시간인 평일, 오전 11시마다 전체 팀원이 모여 회의하는 시간을 가졌습니다.

프론트-백 간 전달사항이나 문의사항을 얘기하고, 얘기할 사항이 없으면 각자 개발하러 떠나는 시간이었습니다.

저는 백엔드와 소통이 자주 필요한 채팅과 알림 서버를 맡았기 때문에, 회의 후에는 주로 백엔드 담당자와 회의를 자주 하였습니다.

 

2. 디스코드

체계화된 디스코드를 이용하여 프론트 간 소통, 프론트-백엔드 간 소통을 주로 하였습니다.

 

3. Jira

Jira를 통해 프로젝트 시작 전, 일정을 계획하고 지라의 이슈 키를 통해 Git Branch 이름을 명명하였습니다.

미리 이슈를 정리해둔 덕에 프로젝트 중 해야 할 일을 빠르게 처리할 수 있었습니다.

 

4. Github

https://github.com/goorm-dofarming

 

dofarming

dofarming has 3 repositories available. Follow their code on GitHub.

github.com

 

5. Notion

Notion을 통해 팀 소개 페이지를 작성하고, 프로젝트 관련 문서와 회의록을 정리했습니다.

https://nsoh.notion.site/261efb762d764aa1ab07aa36670330a1

 

구름적 사고 | Notion

“도파밍“ 을 만드는 구름적 사고 팀입니다 💪

nsoh.notion.site


구름톤 풀스택 과정 수료식

 

위치 : 경기 성남시 분당구 판교로 242 PDC A동 9층 (구름 본사)

시간표

  • 10:00-10:10 출석 확인
  • 10:10-10:20 개회식/오프닝
  • 10:20-10:40 커리어매니저 소개
  • 10:40-12:05
    • 10:20 ~ 10:40 1번째 팀 (20분)
    • 10:40 ~ 11:00 2번째 팀 (20분) 
    • 11:00 ~ 11:20 3번째 팀 (20분)
    • 11:20 ~ 11:40 4번째 팀 (20분)
  • 12:05-13:30 점심시간 (먼데이키친, 스테이크덮밥)
  • 13:30-15:20
    • 13:30 ~ 13:50 5번째 팀 (20분)
    • 13:50 ~ 14:10 6번째 팀 (20분)
    • 14:10 ~ 14:30 7번째 팀 (20분)
    • 14:30 ~ 14:40 쉬는 시간 (10분)
    • 14:40 ~ 15:00 8번째 팀 (20분)
    • 15:00 ~ 15:20 9번째 팀 (20분)
  • 15:20-15:30 쉬는시간
  • 15:30-15:50 심사위원 팀별 총평
  • 15:50-16:10 쉬는시간 및 점수 취합
  • 16:10-16:20 파이널 프로젝트 시상식 및 수료보고
  • 16:20-16:40 만족도 조사 및 교육 종료 후 안내 사항 공지
  • 17:00-19:00 플레이어 및 멘토 간 네트워킹 (우수팀, 우수수료생 인터뷰)

 

우리 팀은 발표가 1번이었고... 저는 발표자였습니다.

사실 오프라인 발표는 후반부로 갈 수록 기억에 잘 남아서 유리하다고 생각했는데, 이번에 그런 생각이 조금 바뀌었습니다.

다들 프로젝트를 하느라 체력이 고갈되고 아침 일찍까지 판교에 오느라 피곤한 상태였습니다. 그래서 1번이었던 저희 팀 발표때에는 그래도 다들 집중을 하였는데 뒤로 갈수록 다들 졸려하고 집중력이 떨어지는게 보였습니다.

특히 점심시간 이후 첫번째 팀때에는 늦게 복귀한 사람들과 식곤증 덕에 조금 어수선한 분위기였습니다.

저는 긴장을 잘하는 편이지만, 말투나 표정에서는 그 부분이 드러나지 않아 발표에 유리한 편이라 생각합니다.

그런데 저 날은 정말 다리가 덜덜 떨릴만큼 긴장을 했습니다... 앞에 단상이 있어서 다행이었죠 ㅜ

점심은 구름에서 제공해줍니다 ! 저는 구름 근처의 '먼데이키친'이라는 식당에서 밥을 먹었는데, 발표가 끝났음에도 아직 긴장이 안풀려서 밥이 도저히 안넘어가더라구요..

그리고 식당에서 미리 음식을 세팅해놓고 테이블에 모두 앉히는데, 저희 팀 옆에 두자리가 남았고 프로젝트 심사해주시는 멘토님 두분이 오셔서.. 부담스럽진 않았지만 자꾸 멘토님들 대화에 귀가 쏠렸습니다..

모든 발표가 끝나고 시상을 했는데 상은 두가지 '우수상'과 '인기상'이 있습니다. 제 생각에 1등은 우수상 2등은 인기상인 것 같아요 ㅎㅎ

그리고 저희 팀은..

인기상을 받았습니다 !!

인기상은 부상이 없는 줄 알았는데 외식상품권이라는 이름으로 배민 3만원권을 각각 받았습니다 ㅎㅎ

사실 우수상도 기대했는데 정말 발표를 압도적으로 잘하고 실제 서비스처럼 베타 테스트를 진행한 팀이 있었어요.

우수상 받으려면 정말 실 서비스처럼 해야할 것 같습니다..

그래도 좋은 결과로 끝나서 보람차고 뿌듯한 프로젝트였어요 😊


프로젝트 좋았던 점 / 아쉬운 점

좋았던 점

  1. 그래도 구름에서 진행하는, 현재 팀원들과 진행하는 두번째 프로젝트라 그런지 소통이 원활하고 필요한 것과 수정할 것을 빠르게 피드백 받을 수 있었습니다.
  2. 이 전의 어떤 프로젝트에서도 해보지 못한 실시간 채팅과 알림 기능을 구현하며 새로운 기술들을 접해볼 수 있었습니다.
  3. Web IDE 프로젝트때에는 시간에 쫓겨 매일 밤을 새다시피 개발을 진행했는데, 이번에는 일정을 체계적으로 잡고 매일 할 일을 해내다보니 밤을 새지 않아도 기능을 완성할 수 있었습니다.
  4. 프로젝트 마감일로부터 여유롭게 구현이 끝난 덕에 배포된 서버로 테스트를 하고 수정할 부분을 모두 고쳐 완벽한 웹 사이트의 상태로 발표를 해서 마음이 여유로웠습니다.
  5. 의견 충돌이 많았던 기획 회의 때에도 열정적이었던 팀원들 덕에 현재의 좋은 아이디어가 나온 것 같습니다.
  6. 필수 기능의 제약 없이 하고 싶은 기능, 하고 싶은 디자인을 구현하며 재미있었습니다.

 

아쉬운 점

  1. 새로운 기능을 적용시키는데에 급급하여 하나하나 이해하지 못하고 넘어간 것이 아쉽습니다. 이제 교육도 끝났으니 사용했던 기술들을 하나씩 짚으며 공부할 것입니다.
  2. 배포를 직접 해보지 못한 것이 아쉬운데, 프론트엔드 3명 중 1명이 배포를 도맡아 하다보니 도움을 주기가 힘들었던 점이 아쉬웠습니다.

마무리

6개월간 달려왔던 구름톤이 끝나며 마냥 시원할 줄 알았는데,

막상 섭섭하기도 하고 소속감이 사라져서 조금 무섭기도 합니다

그래도 2개의 프로젝트를 진행하며 성장한 것 같아 뿌듯하기도 합니다

이제는 정말 취준생으로서 회사에 1인분을 할 수 있는 사람이 되도록 노력해야겠죠..

좋은 팀원들, 좋은 프로젝트들, 좋은 멘토님들을 얻어갈 수 있었던 구름톤 풀스택 과정이었습니다 😊

'회고록' 카테고리의 다른 글

[구름톤] Web IDE 만들기 프로젝트 회고록  (1) 2024.07.08

구름톤 풀스택 개발자 양성과정 7회차를 수강하며 진행했던 Web IDE 만들기 프로젝트입니다.

우선, 필수적으로 들어가야하는 기능이 있는데요.

  1. Web IDE의 기본적인 틀과 사용자 인터페이스(UI) 설계
  2. 회원 가입 및 로그인 기능 구현 
  3. 코드 편집기 기능 구현
  4. 채팅 기능 구현

위의 4가지 기능이 들어간 Web IDE를 만드는 것이 프로젝트의 목표입니다.

 

저희 팀은 최초에 FE 3명, BE 4명으로 구성되었으나,

기획 기간이 끝남과 동시에 FE 한분이 나감으로서, FE 2명이서 모든 것을 개발해야했습니다 🥲

 

- 기획 기간 : 2024.05.28 ~ 2024.06.07

- 프로젝트 기간 : 2024.06.08 ~ 2024.06.24

개발기간이 굉장히 짧았고, 저는 리액트로 프로젝트를 진행하는게 처음이라 어려움이 많았습니다.

하지만 팀원들과 함께 해내었고 리액트와 많이 친해질 수 있었던 계기가 되었습니다.


프로젝트 개요

- 프로젝트 <Study For Coding>

- 여러 스터디들을 체계적으로 관리하기 위한 WEB IDE

- 알고리즘 문제에 대하여 팀원 간의 빠른 피드백과 실시간 코드 리뷰를 위한 WEB IDE

 

프로젝트 주요 기능

  1. 회원 가입/로그인
  2. 코드 편집기 기능
  3. 실시간 코드 리뷰 (채팅처럼)
  4. 문제 검색 기능
  5. 문제 해결 여부 확인 기능

 

완성된 프로젝트 시연 영상

 


기술 스택

Frontend

  • TypeScript
  • React
  • styled-components
  • recoil
  • pnpm
  • axios
  • React-cookie
  • highlight.js
  • MUI

 

Backend

  • Spring boot
  • Java 17
  • MySQL
  • MongoDB
  • H2
  • Spring Data JPA
  • Spring Data Mongo
  • Gradle
  • STOMP
  • Spring security
  • JWT
  • JUnit5

 

배포 서버로는 AWS EC2, AWS S3,  Docker를 사용하였습니다.


담당 기능

제가 담당한 기능은 아래와 같습니다.

스터디 CRUD

: 스터디 이미지는 서버에 이미지를 업로드 후 서버에 저장된 이미지의 경로를 받아오는 식으로 구현했습니다

카테고리 CRUD

: 스터디 데이터와 연동하여 카테고리를 생성할 수 있고, 카테고리에 문제를 추가할 수 있도록 합니다

코드 작성 및 제출 구현

: highlight.js를 이용해 코드를 하이라이팅 (사용자가 실제로 입력하는 TextArea를 숨기고 그 위의 div에 코드를 띄우는 방식)

: 사용자가 입력하는 코드에 따라 가변하는 TextArea의 높이

: 코드 제출 후 제출 결과를 Backend에서 받아와 사용자에게 띄워줌

: 스터디 내 제출자 / 미제출자 구분하여 리스트

알고리즘 검색 기능 구현

: 검색 창에 특정 문자 또는 숫자를 입력 시 해당하는 문제만 보이도록 함 (검색 알고리즘은 Backend에서 구현)

전반적인 Front CSS 수정

: 부드럽게 접었다 펴지는 사이드바, 알고리즘 리스트의 가변 너비, 스크롤 바 등 사용자 편의성을 높이는 CSS로 수정


프로젝트 소통 및 진행 방식

1. 데일리 스크럼 및 매일 회의

교육 시간인 평일, 오전 11시마다 전체 팀원이 모여 회의하는 시간을 가졌습니다.

프론트-백 간 전달사항이나 문의사항을 얘기하고, 얘기할 사항이 없으면 각자 개발하러 떠나는 시간이었습니다.

또한, 프로젝트 기간이 짧기에 데일리 스크럼은 평일, 주말 매일 작성하였습니다.

 

2. 디스코드

체계화된 디스코드를 이용하여 프론트 간 소통, 프론트-백엔드 간 소통을 주로 하였습니다.

 

3. Jira

Jira를 통해 프로젝트 시작 전, 일정을 계획하고 지라의 이슈 키를 통해 Git Branch 이름을 명명하였습니다.

미리 이슈를 정리해둔 덕에 프로젝트 중 해야 할 일을 빠르게 처리할 수 있었습니다.

 

4. Github

https://github.com/study-for-code

 

study-for-code

study-for-code has 2 repositories available. Follow their code on GitHub.

github.com


프로젝트 좋았던 점 / 아쉬운 점

좋았던 점

  1. 처음 진행해보는 리액트 프로젝트였지만 다른 FE분이 능숙하셔서 모르는 것을 물어보고 틈틈히 배울 수 있었습니다.
  2. 모든 것을 처음해보기에, 어떤 결과였더라도 저는 성장했을 것 입니다. 하지만 프로젝트의 목표를 완전히 끝냄으로서 많이 성장할 수 있었습니다.
  3. 교육만 들을 때는 알 수 없었던 Recoil이나 Styled-Component를 사용하며, 기존에 HTML/CSS를 사용할 때보다 리액트가 훨씬 더 합리적인 언어라는 것을 알 수 있었습니다.
  4. 협업을 진행하며 서로 지켜야 할 개발 규칙도 배울 수 있었습니다. (주석 달기 및 Prettier 사용, Git 사용 규칙 등)
  5. 백엔드와의 소통을 실제로 진행하며 프론트 작업만 할 때는 몰랐던 것을 알 수 있었습니다. (Axios를 통해 백엔드와 통신하는 방법, 백엔드에서 받아온 결과 확인 방법 등)
  6. "나는 잘 모른다 !" 라는 마음으로 궁금한 점이나 해결할 수 없는 오류가 발생하면 팀원들에게 많이 물어봤습니다. 빠르게 해결을 도와줬던 팀원들 덕에 혼자 생각하며 시간을 버리기보다 더 유용했습니다. 또한 오류를 해결하는, 방지하는 팁도 얻을 수 있었습니다.

 

아쉬운 점

  1. 짧았던 기간 탓에 Recoil, Styled-Component, Axios 등 많은 기술들을 공부하기보다는 적용시키기에 급급했습니다. 일단 적용하고 이해하며 코드를 작성하여 기술들을 완벽히 공부했다 할 수는 없습니다.
  2. 리액트로 화면을 구성하기에도 벅찼던 덕분에 프로젝트 배포나 CI/CD 관련하여 전혀 해보지 못했습니다.
  3. 하루하루 했던 일을 기록하거나 오류 해결 후 짧게나마 끄적여둘걸 하고 후회합니다. 같은 오류가 또 발생할 수 있을텐데 시간에 쫓겨 하지 못한 것이 아쉽습니다.

마무리

구름톤의 1차 프로젝트가 끝이나고 2차 프로젝트를 시작하며 이 글을 씁니다.

2차 프로젝트 때는 더 나은 개발자가 되어있길..! 2차 프로젝트 회고록에는 아쉬운 점이 줄어들었길 바랍니다 🙏

분할정복(Divide&Conquer)이란?

분할정복(Divide & Conquer) 알고리즘은 문제를 해결하는 일반적인 방법 중 하나로, 주어진 문제를 더 작고 동일한 유형의 하위 문제로 나눈 다음, 이 하위 문제들을 해결하여 원래 문제의 해답을 구하는 방식입니다. 이 방법은 여러 가지 알고리즘 설계 기법 중에서도 특히 강력하고 널리 사용됩니다.

분할정복 알고리즘은 세 가지 주요 단계로 구성됩니다:

  1. 분할(Divide): 문제를 더 작고 독립적인 하위 문제로 나눕니다. 이 단계에서는 주어진 문제를 나누는 기준과 방법을 결정합니다.
  2. 정복(Conquer): 나눈 하위 문제들을 재귀적으로 해결합니다. 이 단계에서는 하위 문제를 해결하기 위해 다시 분할정복 알고리즘을 적용하거나, 더 이상 나눌 수 없을 만큼 작은 경우 직접 해결합니다.
  3. 결합(Combine): 하위 문제의 해답들을 결합하여 원래 문제의 해답을 만듭니다. 이 단계에서는 하위 문제들의 해답을 이용해 전체 문제의 해답을 도출하는 방법을 정의합니다.

 

DP와의 공통점 및 차이점

공통점

  • 큰 문제를 더 작은 하위 문제로 나누어 해결한다.

차이점

  • 분할 정복 : 하위 문제가 서로 독립적
  • DP : 하위 문제들이 중복 되는 경우

 

알고리즘을 공부 할 수록, 서로 비슷한 알고리즘이 많다는 것을 알게 된다. 비슷하지만 다른 알고리즘들을 확실히 이해하고 각 문제에 적합한 알고리즘을 찾는 능력을 키우는 것이 중요해진다.

 

문제 요구사항

재귀적인 패턴으로 별을 찍어 보자. N이 3의 거듭제곱(3, 9, 27, ...)이라고 할 때, 크기 N의 패턴은 N×N 정사각형 모양이다.

크기 3의 패턴은 가운데에 공백이 있고, 가운데를 제외한 모든 칸에 별이 하나씩 있는 패턴이다.

***
* *
***

N이 3보다 클 경우, 크기 N의 패턴은 공백으로 채워진 가운데의 (N/3)×(N/3) 정사각형을 크기 N/3의 패턴으로 둘러싼 형태이다. 예를 들어 크기 27의 패턴은 예제 출력 1과 같다.

 

입력

첫째 줄에 N이 주어진다. N은 3의 거듭제곱이다. 즉 어떤 정수 k에 대해 N=3^k이며, 이때 1 ≤ k < 8이다.

 

출력

첫째 줄부터 N번째 줄까지 별을 출력한다.

 

입출력 예

입력 출력
27

 

문제 이해하기

문제 설명이 자세하지 않아 문제를 이해하는데 시간이 걸렸다.

 

이해한 결과, 3의 제곱수인 N이 주어질 때, N은 다음과 같이 구성된다.

  • N이 1일 때
*
  • N이 3일 때 : 가운데에 크기가 3/3 = 1*1인 공백을 두고 3*3의 패턴이 완성된다.
***
* *
***
  • N이 9일 때 : N이 3일 때의 패턴을 반복한다. 단, 가운데에 크기가 9/3 = 3*3인 공백을 두고 패턴을 그린다.

  • N이 27일 때 : N이 9일 때의 패턴을 반복한다. 단, 가운데에 크기가 27/3 = 9*9인 공백을 두고 패턴을 그린다.

즉, N이 커질수록 N/3에서 사용한 패턴을 재사용하여 3X3의 패턴을 만드는데, 중앙은 빈 형태로 그리게 되는 것이다.

그럼 이 문제는 하위 패턴을 재사용한다는 점에서 DP가 아닐까 싶긴하지만, 그림이 아닌 문자열을 하나씩 찍어나가는 것이기에, DP보다는 분할정복이 더 적합하다고 볼 수 있다.

 

성공 코드

function solution(N) {
    // 2차원 배열 생성 후 각 자리의 문자 저장 -> 마지막에 join
    let pattern = Array.from({ length: N }, () => Array(N).fill(' '));

    function fillPattern(x, y, size) {
        if (size === 1) {
            pattern[x][y] = '*';
            return;
        }

        let newSize = size / 3;
        for (let i = 0; i < 3; i++) {
            for (let j = 0; j < 3; j++) {
                if (i === 1 && j === 1) continue; // 가운데 비우기
                fillPattern(x + i * newSize, y + j * newSize, newSize);
            }
        }
    }

    fillPattern(0, 0, N);

    for (let i = 0; i < N; i++) {
        console.log(pattern[i].join(''));
    }
}

solution(N[0]);
  1. 문자열을 담을 2차원 배열을 생성한다 -> N*N 배열
  2. fillPattern 함수를 재귀적으로 호출하며 배열을 채운다
    • fillPattern(0, 0, 9)
      1. fillPattern(0, 0, 3) -> fillPattern(0, 0, 1) -> [0, 0] 자리에 *
      2. 반복하여 [ 0~2, 0~2 ] 자리에 3X3 패턴 생성
      3. fillPattern(0, 3, 3) -> fillPattern(0, 3, 1) -> [0, 3] 자리에 *
      4. 반복하여 [ 0~2, 3~5 ] 자리에 3X3 패턴 생성
      5. 위의 과정 반복하며 한 패턴씩 생성하여 9X9 패턴 완성
    • 위의 9X9 패턴 생성 반복
  3. 마지막에 배열을 join함으로서 문자열 형태로 출력

 

지피티의 도움을 받아 문제를 풀었다. 처음에는 단순 패턴 반복이라 생각하였는데, 너무나도 인적 사고였고.. 한 줄씩 출력하며 패턴을 완성해야하는 프로세스를 구현하기가 어려웠다. 컴퓨터적 사고를 통해 알고리즘을 구현해내는 것을 더 연습해야 할 듯 하다.

문제 요구사항

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.

예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {1020, 10, 30, 20, 50} 이고, 길이는 4이다.

 

입력

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000)이 주어진다.

둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (1 ≤ Ai ≤ 1,000)

 

출력

첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이를 출력한다.

 

입출력 예

입력 출력
6
10 20 10 30 20 50
4
  • 처음에는 문제가 잘 이해되지 않았다
  • 배열 [10, 20, 10, 30, 20, 50] 으로 만들 수 있는 부분 배열 중, 숫자가 연속적으로 커지는 배열의 최대 길이를 구한다
  • 예를 들어, [10], [10, 20], [10, 20, 30] 은 연속적으로 숫자가 커지는 배열이지만,[10, 20, 10], [10, 30, 20] 은 연속적으로 커지지 않기 때문에 답이 될 수 없다
  • 위의 입출력 예시에서는 [10, 20, 30, 50] 배열이 연속적으로 이어지는 부분 배열 중 길이가 가장 긴 배열이라 답이 4가 된다

 

첫번째 시도 - 문제 이해를 잘못함

처음에는 배열 내 앞의 숫자보다 커지는 수의 개수를 말하는 줄 알고 코드를 잘못 작성하였다. 당연히 오답 !

 

두번째 시도 - 여전히 문제 이해를 잘못함

이번에는 배열 내 앞의 숫자보다 커지는 수를 새로운 배열로 생각 했을 때, 그 배열의 최대 길이인 줄 알고 아래 코드를 작성했다.

function solution(A) {
    let n = +A[0]
    let nums = A[1].split(' ').map(Number);
    let maxLength = 0;

    for(let i = 1; i < n; i++){
        let cnt = 1;
        let increaseSet = new Set();
        for(let j = 0; j < i; j++){
            if(nums[j] < nums[i]) {
                if(!increaseSet.has(nums[j])){
                    increaseSet.add(nums[j]);
                    cnt++;
                }
            }
        }
        if(cnt > maxLength) maxLength = cnt;
    }

    console.log(maxLength);
}

당연히 오답 !

 

성공 코드

지피티의 도움으로 문제를 완전히 이해하고 dp를 적용시켜 문제를 풀어보았다.

  1. '연속적'으로 증가하는 부분 배열을 구하기 위해, 이중 for문을 사용한다.
  2. 첫번째 for문은 1번 인덱스부터 마지막 인덱스까지, 그 안의 두번째 for문은 0번째 인덱스부터 i-1번째 인덱스까지 돈다.
  3. 모든 인덱스는 요소 1개(본인)를 가지고 있기에, 배열의 길이와 같은 배열을 생성하여 모든 값을 1로 채운다. → dp배열
  4. for문에서 0부터 i-1까지 돌며 nums[i]보다 작은 값이 있으면 이 인덱스의 dp 값과 현재 dp 값을 비교하여 더 큰값을 할당한다.
    1. dp 배열은 모두 1로 채워져있고, 앞의 값부터 계산하게 된다.
    2. 예를 들어 i = 1, j = 0 일 때, nums[0]이 nums[1]보다 작은 값이라면 dp[1]에는 1 + 1인 2가 저장된다.
    3. 다음으로, i = 2, j = 1일 때, nums[1]이 nums[2]보다 작은 값이라면 dp[2]에는 dp[1] + 1인 3이 저장된다.
    4. 이 때 Math.max 함수를 사용하는 이유
      • nums[ j ]가 nums[ i ]보다 작은 값일 때, dp[ i ]에 값이 할당 되는데, 만약 i = 4라고 가정하자.
      • dp[2] = 3, dp[3] = 1 인 상태에서, nums[2], nums[3]이 nums[4] 보다 작은 값이라면 for문은 순차적으로 돌아 먼저 dp[4]dp[2]+1 = 4가 된다. 또 for문을 돌아 j가 3이 되었을 때, dp[3] = 1, dp[4] = 4이기에 이 때는 dp[4]가 4가 되어야한다.
      • 즉, 연속적으로 커지는 배열의 길이 중 최댓값을 구해야하기 때문에 Math.max 함수를 사용해야한다.
  5. 따라서 본 문제의 핵심은 누적값 재사용이라는 것을 알 수 있다.
function solution(A) {
    let n = +A[0]
    let nums = A[1].split(' ').map(Number);
    let dp = Array(n).fill(1);

    for (let i = 1; i < n; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }

    let maxLength = Math.max(...dp);

    console.log(maxLength);
}

solution(A);

 

아직 혼자 생각해내기에는 어려움이 많은 것 같다. 공부를 더 해야겠다...🥲

DP란?

DP = Dynamic Programming (동적 계획법)

복잡한 문제를 간단한 여러 개의 문제로 해결하는 알고리즘 설계 기법

각 하위 문제의 해결을 계산한 뒤, 그 해결책을 저장하여 후에 같은 하위 문제가 나왔을 경우 그것을 간단하게 해결할 수 있다.

DP 적용 조건

  1. Overlapping Subproblems(중복되는 하위 문제)
    • DP는 기본적으로 문제를 나누고 그 문제의 결과 값을 재활용해서 전체 답을 구한다.
    • 그래서 동일한 작은 문제들이 반복하여 나타나는 경우에 사용이 가능하다.
  2. Optimal Substructure(최적 부분 구조)
    • 부분 문제의 최적 결과 값을 사용해 전체 문제의 최적 결과를 낼 수 있는 경우를 의미한다.
    • 그래서 특정 문제의 정답은 문제의 크기에 상관없이 항상 동일하다.

문제 요구사항

어떤 동물원에 가로로 두칸 세로로 N칸인 아래와 같은 우리가 있다.

이 동물원에는 사자들이 살고 있는데 사자들을 우리에 가둘 때, 가로로도 세로로도 붙어 있게 배치할 수는 없다.

이 동물원 조련사는 사자들의 배치 문제 때문에 골머리를 앓고 있다.

동물원 조련사의 머리가 아프지 않도록 우리가 2*N 배열에 사자를 배치하는 경우의 수가 몇 가지인지를 알아내는 프로그램을 작성해 주도록 하자.

사자를 한 마리도 배치하지 않는 경우도 하나의 경우의 수로 친다고 가정한다.

입력

첫째 줄에 우리의 크기 N(1≤N≤100,000)이 주어진다.

출력

첫째 줄에 사자를 배치하는 경우의 수를 9901로 나눈 나머지를 출력하여라.

입출력 예

N
return
4
41
  • 사자가 0마리 일 때 경우의 수 1
  • 사자가 1마리 일 때 경우의 수 8
  • 사자가 2마리 일 때 경우의 수 18
  • 사자가 3마리 일 때 경우의 수 12
  • 사자가 4마리 일 때 경우의 수 2

첫번째 시도 - 구현 실패

일단은 생각나는 대로 코드를 작성해보았다.

  1. 사자가 0마리 일 때에는 경우의 수가 항상 1이다
  2. 사자가 1마리 일 때의 경우의 수는 전체 칸 수 이다
  3. 사자가 2마리 일 때의 경우의 수는 1부터 N-1개의 홀수에 각각 2를 곱한뒤 더한다

하지만 이 이후로 알고리즘을 생각해내기가 어려웠다.

(N=3의 경우의 수) + (N=4만 가능한 경우의 수)가 답 인것 같은데, 그 로직이 떠오르지 않아 골머리를 앓았다.

혼자 2시간 쯤 고민을 하고 결국 GPT의 도움을 받았다.

성공 코드

  1. 사자가 있는 우리를 배열로 나타내는게 아니라, i번째 줄에 사자가 없는 경우, 1마리씩 있는 경우를 배열로 나타낸다
    • dp[i][0] = i번째 줄에 사자가 없는 경우
    • dp[i][1] = i번째 줄 첫번째 열에 사자가 있는 경우
    • dp[i][2] = i번째 줄 두번째 열에 사자가 있는 경우
  2. 배열의 크기가 (N+1) * 3 인 배열을 만든다
    • 사실 배열의 크기가 Nx3 이어도 되는데, 가독성을 위해 0번째 행은 제외하고 생각한다.
  3. dp[1][0], dp[1][1], dp[1][2] 는 모두 1이다
    • 1번째 줄에 사자가 없을 경우 1가지
    • 1번째 줄 첫번째 열에 사자가 있을 경우 1가지
    • 1번째 줄 두번째 열에 사자가 있을 경우 1가지
  4. i번째 줄의 경우의 수는 아래와 같이 구한다
    • dp[i][0] = (dp[i-1][0] + dp[i-1][1] + dp[i-1][2])
      • i번째 줄에 사자가 없다면, i-1번째 줄에는 사자가 없어도 되고 있어도 되기에 모든 경우의 수를 더한다
    • dp[i][1] = (dp[i-1][0] + dp[i-1][2])
      • i번째 줄 첫번째 열에 사자가 있다면, i-1번째 줄에는 사자가 없거나, 두번째 열에 있을 수 있다
    • dp[i][2] = (dp[i-1][0] + dp[i-1][1])
      • i번째 줄 두번째 열에 사자가 있다면, i-1번째 줄에는 사자가 없거나, 첫번째 열에 있을 수 있다

위의 내용을 정리하면 아래 그림과 같다.

하위 → 상위 식으로 값이 누적되어 최종적으로는 N번째 줄의 모든 값을 더 하면 답이 된다.

function solution(N) {
    const mod = 9901;
    const dp = Array.from({ length: N + 1 }, () => Array(3).fill(0));
    
    // 초기값 설정
    dp[1][0] = 1;
    dp[1][1] = 1;
    dp[1][2] = 1;
    
    for (let i = 2; i <= N; i++) {
        dp[i][0] = (dp[i-1][0] + dp[i-1][1] + dp[i-1][2]) % mod;
        dp[i][1] = (dp[i-1][0] + dp[i-1][2]) % mod;
        dp[i][2] = (dp[i-1][0] + dp[i-1][1]) % mod;
    }
    
    const result = (dp[N][0] + dp[N][1] + dp[N][2]) % mod;
    console.log(result);
}

처음 지피티의 코드를 보고 이해하기가 쉽지 않았다.

이해를 한 뒤에는, 이런식으로도 문제를 풀 수 있구나하는 생각에 뒤통수를 맞은 느낌이었다.

마냥 사자의 수와 우리에 얽매여 더 효율적인 프로세스를 생각하지 못한것이 안타깝다..🥲

+ Recent posts