질문과 답변을 통해 마음을 열고 대화 나누는 소통 플랫폼인 '오픈마인드'
자유롭게 질문을 생성하고 답변 할 수 있습니다.
또한 링크를 통해서 질문과 답변을 공유할 수 있습니다.
피드 생성
메인페이지에서 이름을 입력하고 '질문 받기' 버튼을 클릭하면 피드를 생성
질문 리스트
정렬 순서를 최신순, 이름순으로 설정
질문 하기
'질문 작성하기' 버튼을 클릭하여 질문 등록
질문에 '좋아요' '싫어요' 기능
답변 하기
질문에 대한 답변 기능
답변 거절 기능
답변 수정 기능
질문 삭제 및 모두 삭제 기능
질문에 '좋아요' '싫어요' 기능
링크 공유
'링크 아이콘'을 클릭하면 URL을 클립보드에 복사
'카카오 아이콘'을 클릭하면 카카오톡으로 공유 가능
'페이스북 아이콘'을 클릭하면 페이스북으로 공유 가능
Github Flow
R&R 분배
전FLOW CHART
를 통한 공정 흐름 이해
기능 별 난이도 1, 2, 3 으로 치환, 10점씩 역할 분배, 역량 수준 고려하여 예상 일정 수립, 리팩토링, 버그픽스, 추가기능 구현 일정 추가 계획.
보일러 플레이트
(boiler plate
) 세팅2024.01.17(수) ~ 2024.02.01(목)
eslint
, prettier
, husky
) 이슈👍🏻 잘한점, 유지하고 싶은점
당장 내일 사용해야 하는 팀 프로젝트 세팅을 밤을 새서 완료했다. eslint
, prettier
, husky
를 설치했고, github
프로젝트 세팅도 완료했다. 처음 접한 사람의 입장에서 프로젝트 세팅은 쉬운일이 아니었다. 정해놓은 목표를 달성 할 때 까지 포기하지 않고 끊임없이 노력한 점을 유지하고 싶다.
🤔 문제점, 문제점을 해결하기 위한 노력
eslint
, husky
를 설치하는 것은 생각보다 많은 어려움이 없었지만 이 자체를 이해하는 것에서 어려움을 느꼈다. eslint
가 무슨 기능을 하는지, 그리고 어떻게 동작하는지를 공부하는 것. 그리고 그 전에 근본적인 것을 모르고 있어서 곤란했던 상황도 있었다. npm
이 뭔지, package.json
이 왜 있는지, 이런 기본적인 의문조차 가지지 않고 그냥 해왔는데 그게 화근이 되었다. npm install
이 작동하는 원리를 알게되니, 팀 프로젝트 세팅에 대해서 조금 명확하게 이해할 수 있었다.
husky
가 동작을 안 하는 것이 문제였고, 이 부분 때문에 몇 시간을 허비했다. eslint
가 실행되고 코드에 문제가 있어도 commit
이 잘 되는 문제였다. husky
가 어떻게 설치가 되고 그 기능들을 어떻게 원하는대로 설정하고 동작하게 하는지에 대한 이해가 없으니 어떻게 해결해야 할지도 모르고 계속 헤맨 것이다. 하지만 package.json
에 대해 이해를 하니, husky
가 동작하지 않는 이유에 대한 가설을 세울 수 있게 되었다. 문제는 husky
세팅 폴더 안의 husky
실행 조건이 push
할 때만 lint
를 실행하는 것이었다. 그래서 commit
을 할 때에도 lint
를 실행하게끔 코드를 추가했고, 그 후로 정상적으로 작동을 하게 되었다.
문제 : 처음 작업 때 branch
를 main
으로 만들고 커밋을 했다. 그리고 그 후에 feature
branch
를 생성해서 쭉 진행했다. (처음 하나의 commit
만 main
이고 나머지는 전부 feature
인 상태)
main
branch
로 제일 첫번째 commit
이 하나 생성 되어있는것을 인지하지 못하고 feature
branch
를 새로 생성해서 쭉 commit
하며 작업해서 작업들이 많이 쌓여있었다. feature
branch
만 팀 레포지토리 develop
브랜치에 pr
요청 올리고 merge
를 하는거라서, main
브랜치는 팀 레파지토리에 올라가지 않는다. (해당 내용을 깨닫기 위해 길고 긴 시행착오가 있었다.)
작업한 feature
를 pr
올리고 머지하면 팀 레파지토리의 develop
브랜치는 제일 최신 상태를 유지한다. 그리고 그것을 내 리모트 레파지토리에 싱크 포크하면 내 리모트 레파지토리는 최신 상태를 유지한다. 그리고 내 리모트 레파지토리의 main
은 초기 상태 그대로이다. 왜냐? main
을 merge
하지 않았으니까. (어차피 main
은 clone
해오지도 않을거다. 지금 내가 필요한건 작업중인 develop
이니까 마지막까지 main
은 초기 상태를 유지하는 게 맞는 것이다.)내 로컬 레파지토리를 다 지운다. 그리고 최신 상태인 내 리모트 레파지토리의 develop
브랜치만을 clone
해오면 잘못 만든 main
브랜치가 삭제되고 어떤 오류도 발생하지 않는다.
텍스트 입력창, 버튼 부분이 꽉 채워지지 않던 문제
입력창과 버튼을 감싸는 div
에 flex-grow: 1;
을 주었음에도 늘어나지 않았다. 안의 요소들과 감싸는 div
태그들에 width: 100%;
을 주지 않아서 생긴 문제였다.
컴포넌트화를 한 이후에 사진처럼 쪼그라드는 문제
question-card
의 부모 요소들에 flex
를 넣고, width
를 100%를 넣어봐도 계속 바뀌지 않았다.
컴포넌트로 생성된 이상한 이름 클래스네임을 가진 div
가 문제인 것 같아서 해당 태그의 부모요소 div
를 만들어서 flex
를 넣어봤지만 변하는 게 없었다.
여기서 실수 2개. 해당 부모 요소에 width: 100%
를 주지 않은 것. 그리고 스타일드 컴포넌트 바깥에 부모 div
를 만들어놓고 styled component
안에서 해당 div
에 스타일을 지정해준 것.
이렇게 스타일드 컴포넌트 제일 첫줄에 width
를 주니 굳이 div
부모요소를 만들어줄 필요도 없고 바로 부모요소로 적용이 됐다.
eslint
삼항연산자
중복 선언 금지 규칙, if else
선언문 규칙 이슈삼항연산자
를 두 번 썼을 때 eslint
오류가 발생했다. 가독성 문제 때문에 따로 if else
를 사용하라는 규칙이다. 그래서 삼항연사자를 지우고 따로 함수를 만들어서 if else
를 사용하기로 결정했고, else if
를 사용하니 이번에는 Unnecessary 'else' after 'return’
라는 eslint
오류가 발생했다. 이는 if
문 후에 불필요한 else
를 붙이는 것도 가독성에 문제가 있다는 규칙이다. else
대신 return
만 사용해도 else
역할을 하니, else
를 사용하지 말라는 규칙이다.옵셔널 체이닝
이슈
?.
를 붙이지 않으니 오류가 발생했다. ?.
는 optional chaining
이라는 java script
문법이다. 객체가 Null
, undefined
인 경우에 error
를 발생시키지 않고 대신 Undefined
를 반환하는 문법이다. 위에 상황을 예로들어서 옵셔널 체이닝을 사용하면 question
이나 question.answer
이 없는 경우에도 코드가 안전하게 실행될 수 있다. question
이나 question.answer
이 항상 존재한다고 보장할 수 없는 상황에서 유용하다. 그 밑에 옵셔널 체이닝을 사용하지 않은 이유는, question.answer
이 존재하지 않을 경우의 return
값을 미답변으로 지정해줬기 때문이다.eslint
버튼 태그 외 클릭 이벤트 선언 이슈OnClick
속성을 주면 에러가 발생한다. 그럴 때 role=”presentation”
을 주면 해결된다. 아주 간단히 요약하면, 버튼 태그의 기능을 그대로 사용할 수 있지만 버튼태그로써의 접근성을 지워주는 역할을 한다. div
태그처럼 아무 의미없는 태그가 되는 것이지만 버튼 태그의 기능은 사용할 수 있는 것이다.공식문서 : https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/presentation_role
textarea
에 onKeyDown
으로 선언한 함수를 넣어줘서 엔터키로 데이터 정상 전송이 된다. 하지만 서버에 등록돼서 받아오는 데이터는 줄바꿈이 적용되지 않았다. 해당 문제는 데이터를 출력해주는 클래스에 white-space:pre-line
을 선언해주니 해결되었다.setClickStatus
를 상위 컴포넌트에서 Props
로 받아왔다.dropdownRef
로 케밥 최상위 요소에 ref
를 심어서 참조를 하고,useEffect
훅을 사용하여 드롭다운 메뉴 외부를 클릭 했을 때 드롭다운 메뉴를 닫는 기능을 구현한다.e.target
은 내가 클릭한 요소를 확인하는 요소이니까 dropdownRef.current.contains(e.target)
이 코드는 내가 지금 클릭한 곳이 드롭다운 안쪽인지 확인을 하는 것이다. (ref
가 드롭다운 부모 요소에 붙어있으니까)addEventListener
는 마우스 클릭 할 때 마다 handleClickOutside
함수를 호출한다. 그리고 리턴문은 클린업 함수이다. 클린업 함수는 컴포넌트가 언마운트 되기 전에 실행되는 것인데, 이벤트 리스너를 제거한다. 이렇게 되면 컴포넌트가 언마운트 된 후에도 이벤트 리스너가 메모리에 남아있지 않게 된다.unmount
란 컴포넌트 생명주기의 마지막 단계이다. 컴포넌트가 DOM
에서 제거되는 단계이다. 조건부 렌더링에 의해서 컴포넌트가 더이상 표시되지 않거나, 사용자가 페이지를 이동해서 다른 컴포넌트가 렌더링 되는 경우에 언마운트가 발생할 수 있다.)문제:
처음 내용을 입력하고 버튼을 누르면 새로고침이 되고 입력이 안 된다.
그리고 다시 내용을 입력하고 버튼을 누르면 정상적으로 등록이 된다.
이 문제를 팀원이 button
태그를 submit
말고 button
으로 바꾸고 post
메서드 이행 후에 새로고침하기위해 submitAnswer
함수 최하단에 window.location.reload(true);
을 넣으라고 말씀해주셔서 그대로 했더니 계속 오류가 발생한다.
해결:
button
에 submit
타입을 줬는데, submit
의 기본 동작에 새로고침이 들어가있다. 그래서 내용이 입력되기 전에 기본적으로 새로고침이 들어가는 것이다. 그래서 두번째 다시 동작 했을 때, 기본 동작에 의해 새로고침이 되면서 전에 썼던 내용이 들어가는 것이다. 이를 해결하기 위하여 버튼 타입을 button
으로 바꾸고 post
메서드 이행 후에 새로고침 명령어를 의도적으로 넣어서 내용을 입력하고 해당 내용이 서버에 post
되면 새로고침이 되는 것이다.
useParams
구조분해 이슈const { id } = question;
이 부분인데, id
가 들어가 있기 때문에 useParams
를 줘야한다고 생각해서 useParams
를 줬는데, 오류가 떴다. 왜 오류가 떴나면, 파람스를 사용하면 지금 보이는 페이지에 해당하는 Id
를 받아오기 때문에 post
이행문에서 요구하는 id
는 지금 보이는 페이지의 Id
가 아니라 질문에 대한 id
이기 때문에 오류가 발생한 것이었다. 그래서 const {id}=question
을 사용하게 되면 question
안에 id
를 받아온다.json
"count": 12,
"next": null,
"previous": "https://openmind-api.vercel.app/3-5/subjects/2805/questions/?limit=100",
"results": [
{
"id": 3698,
"subjectId": 2805,
"content": "뭐 드시냐고 세번째 물어봅니다",
"like": 0,
"dislike": 0,
"createdAt": "2024-01-23T15:37:22.022619Z",
"answer": {
"id": 1949,
"questionId": 3698,
"content": "배 먹어 배!",
"isRejected": true,
"createdAt": "2024-01-23T15:51:21.322068Z"
}
상위 컴포넌트에서 받아온 question
은 results
배열을 매핑해온 것이다.
여기서 Id
는 해당 질문에 대한 질문 Id
이고, subjectId
가 useParams
를 사용하면 받아오게 되는 질문을 받는 대상에 대한 id
이다. 그래서 const { id } = question;
를 사용하게 되면 구조분해로 인하여 id
값 3698
을 사용하게 되는 것이고, 이는 post
에서 요구하는 양식의 Id
와 같다.
jsx
const submitAnswer = async () => {
const response = await fetch(
`https://openmind-api.vercel.app/3-5/questions/${id}/answers/`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content,
isRejected: false,
}),
},
);
여기서 또 하나의 우연이 발생했는데,
jsx
body: JSON.stringify({
content,
isRejected: false,
이 부분은 post
할 때 맞춰야 하는 양식인데, 원래는
jsx
body: JSON.stringify({
questionId : id
content,
isRejected: false,
이렇게 들어가야 한다, 하지만 fetch
로 주소값에서 ${id}
를 보내고 있기 때문에 해당 부분은 자연스럽게 양식을 맞춰서 보내지 않아도 id
가 const { id } = question;
의 id
로 전달이 된 것이다. 즉, 질문 id
가 자동으로 설정되어서 보내진 것이다.
API DELETE
요청 이슈api
딜리트 요청은 따로 리스폰스를 받아오는 게 아니기 때문에 변수를 선언해서 담을 필요가 없다. 즉 기존 api
호출 함수처럼 변수에 담고 그 변수에 json()
을 달지 않아도 된다. (이거 때문에 오류 떴었다.)
갈색 영역이 하얀 영역을 감싸고 있고, 하얀 영역안에 질문 컨텐츠가 렌더링 되는 구조.
일단 질문의 수만큼 하얀 영역이 생성되어야 하지만 그 부분을 해결하지 못해서 팀원의 코드를 참고했다. 반복되는 부분을 컴포넌트화 시켰고, 딱 반복되는 그 컴포넌트 부분만 배열의 map
을 사용해서 반복 시키니 질문의 개수가 새로 생길 때 마다 해당 컴포넌트가 새로 생성이 되었다. (딱 반복되어야 하는 부분을 잘 파악하고 매핑을 하는 것이 중요하다.)
그리고 팀원의 코드를 흡수하며 알아낸 사실 하나. 서버에서 api
를 호출하여 배열을 받아오면 해당 배열을 매핑(map
)해서 사용하기 전 까지는 그냥 prop
으로 전달해줘도 된다. 즉, 사용하기 전 까지 key
값을 전달하고 매핑을 할 필요가 없다는 뜻이다. 반복되어 생성되어야 하는 부분을 잘 파악하고 해당 컴포넌트에 매핑을 해줘야한다.
setContent
가 아니라 setQuestions
를 사용한 이유는 setContent
에서 사용하는 데이터가 question.answer
인데 이 데이터는 상위 컴포넌트의 useState
인 setQuestions
에서 받아와야지 사용 할 수 있기 때문이다. 그래서 상위컴포넌트에서 세터함수를 props
로 받아와서 사용했다.
setQuestions(prevQuewstions ⇒ {
이 부분에 대한 설명은 아래와 같다.
임의로 지정한 인수 prevQuestions
에 setQuestions
의 기존 값이 들어간다.
(세터 함수에는 원래 setQuestion(() ⇒ 값)
이 디폴트인데 ‘() ⇒ ‘
이 부분이 생략된 것이다. 그래서 세터함수에 값을 넣으면 해당 값으로 세터함수를 받는 get
이 변경될 수 있는 것이다.)
그리고 그 값에서 findIndex
를 사용하여 question.id
와 일치하는 값을 찾아서 index
에 저장한다.
const temp = prevQuestions.filter(prevQuestion => prevQuestion);
이 부분은 인덱스로 바로 접근이 안 돼서 딥복사를 하는 과정이다. (새로운 배열을 리턴하는 기능이 필요하기 때문에 filter
를 사용했다. 하지만 map
을 사용해도 기능을 한다. 이 부분에 대한 추가 공부를 따로 하고 아래에 기입할 예정이다.)
⇒
해당 부분은 filter
보다는 map
또는 스프레드 연산자를 사용하는 것이 더 명확하고 적합하기 때문에 map으로 수정했다.
temp[index].answer = result;
위에서 선언해준 index
는 내가 현재 필요한 배열의 index
가 저장되어 있다. 딥복사를 한 temp
배열의 [index]
에 접근해서 해당 배열의 answer
에 접근을하고 그 값에 서버에서 fetch
해온 result
를 넣어준다.
return temp;
그리고 temp
를 리턴해준다.
css
로 skeleton ui
를 구현할 수 있다. useState
로 상태관리를 하며 서버에서 데이터를 받아올 때 스켈레톤 ui를 띄우고 다 받아오면 서버 데이터를 띄운다는 조건문으로 구현할 수 있다.
하지만 편하게 스켈레톤 ui 라이브러리를 설치하면 반짝이는 ui가 자동으로 생성되고, 간단하게 크기 등의 스타일만 넣어주면 된다.
스켈레톤을 구현하며 막혔던 부분은 처음 설치 과정, 그리고 설계되어 있는 코드에 따라서 렌더링이 다르게 나올 수도 있다는 것. 부모 태그와 자식 태그가 컴포넌트로 나뉘어 복잡하게 설계되어 있는 상태에서 자식 태그에 스켈레톤을 적용하면 스타일이 제대로 적용되지 않고 부모태그에 간섭되는 경우가 있었다. 이런 경우에는 마진으로 급하게 마무리 했다. 설치 부분은 import
문제였는데, Skeleton import
문은 항상 상단에 있어야한다. 리액트 임포트문 바로 밑에.
그리고 스켈레톤 css
임포트문은 아무데나 배치해도 상관없다.
스켈레톤 컴포넌트 사용 예시
다음 날 발표인데 Netflify
배포 환경에서 특정 기능이 작동을 안 했다. QuestionPage
의 API
호출이 안 됐고, 이상하게 AnswerPage
의 API
호출은 정상적으로 됐다. 둘은 똑같은 호출 함수를 공유한다. (무한스크롤 함수도 똑같이 공유한다.)
본론으로 들어가기 전, 해당 내용은 무한스크롤 로직(IntersectionObserver API
)에 대한 기본적인 이해가 되어있는 상태에서 읽는 것을 권장한다.
❗️ 문제 상황
DOM
요소와 ref
관련 문제
페이지에 DOM
요소가 렌더링되어야 ref
를 통해 참조된 요소(elementRef.current
)가 실제로 존재하게 된다. 이 요소가 존재해야 Intersection Observer
가 해당 요소의 화면 내 위치를 추적할 수 있다.
로딩 스피너와 스켈레톤 UI의 동시 실행
isLoading
상태가 활성화되는 순간 스켈레톤 UI를 1초 동안 표시하는 로직과 loading Spinner(observer)
를 0.3초 동안 표시하는 로직이 겹쳐서, 스켈레톤 UI와 로딩 스피너(옵저버)가 동시에 실행된다. 하지만, 이벤트 루프(event loop
)에서 처리하는 과정에서 문제가 발생한다.
무한 스크롤과 로딩 스피너 관찰 문제
무한 스크롤 기능의 서버 데이터 로딩 조건은 뷰포트에 로딩 스피너가 보여야 한다는 것이다. 그러나, 스켈레톤 UI가 로딩 스피너보다 길게(1초) 표시되어 로딩 스피너 DOM
이 실제로 존재하지 않는 상황이 발생한다. 이로 인해 elementRef.current
가 null
이 되어, Intersection Observer
가 관찰할 DOM
요소가 없게 된다.
❗️ 해결 과정
원인 파악
스켈레톤을 구현하지 않았을 때로 커밋을 돌렸을 때 무한 스크롤이 정상적으로 동작하는 것을 떠올리며 콘솔 로그를 차근차근 출력해가며 근본적이고 작은 부분부터 문제점을 찾아갔다. 스켈레톤 UI의 지연 시간(1000밀리초)이 로딩 스피너의 표시 시간보다 길어서, 스켈레톤 UI가 활성화되어 있을 때 elementRef.current
가 로딩 스피너를 참조하지 못하는 문제를 발견했다.
해결 방법
useEffect
의 의존성 배열에 elementRef.current
를 추가함으로써, elementRef.current
값에 변화가 있을 때마다 효과를 재실행하여 Intersection Observer
가 새로운 DOM
요소를 관찰할 수 있도록 했다. 이는 elementRef.current
값이 null
에서 실제 DOM
요소로 변화할 때, Observer
가 이를 감지하고 적절히 반응할 수 있게 만들어, 무한 스크롤(infinite scroll
) 기능이 정상적으로 작동하게 한다.
결론
로딩 스피너와 스켈레톤 UI가 겹치면서 발생한 이벤트 루프와 관련된 문제는 elementRef.current
의 변화를 감지하여 Intersection Observer
가 적절히 반응하도록 함으로써 해결되었다. 이 과정에서 useEffect
의 의존성 배열에 elementRef.current
를 추가하는 것이 핵심적인 해결책이었다. 또한, 스켈레톤 UI의 지연 시간을 로딩 스피너(옵저버)를 0.3초보다 낮게 표시하는 로직으로 변경을 해도 정상적으로 작동하는 것을 확인했다.
🥹 아쉬운 점
한 페이지에 여러 사람이 붙어 기능 개발을 하면서 생긴 문제가 꽤 컸다. 팀원들 각자의 코드를 작성하는 방식이 달라서 코드를 이해하는데 오래 걸렸고, 추가 기능을 구현 할 때와 버그 픽스를 할 때 등 유지 보수에 매우 큰 어려움을 겪었다. 이런 방식의 설계는 리팩토링은 필수로 진행을 해주거나, 리팩토링이 필요없을 정도의 컨벤션을 확실하게 설계하고 갔어야 했다고 생각한다. 또한 한 페이지를 한 사람이 맡는 방식의 프로젝트 진행 방식으로 진행해봐도 좋을 것 같다는 생각을 했다. 상대적으로 쉬운 난이도의 페이지가 있다면 해당 팀원이 빨리 끝내고 다른 팀원들을 도와주었다면 더 순조롭게 진행이 가능했을 것 같다.
의견을 자유롭게 제시하고, 그에 반 하는 의견이 있다면 그에 대한 합리적인 이유와 대처방안 등을 부담없이 제시하는 등의 건강한 토론 분위기가 형성되지 못했던 것 같다.
😻 좋았던 점
모든 팀원이 밤을 새서라도 마감 기한을 지켜주는 등의 열정적인 모습을 보여줘서 팀장으로서의 책임감이 더욱 커져서 열심히 하게 되는 원동력이 되었다. 또한 팀원들의 기본 능력이 뛰어나서, 구현에 문제가 생겼을 때 도움을 많이 받았고 덕분에 대부분의 공정이 순조롭게 흘러갔다고 생각한다.
😉 소감
협업 과정에서 발생한 문제들을 명확하게 해결하지는 못했지만, 다음 프로젝트 때 부터는 어떤 식으로 작업을 시작하고 규칙을 짜야할지, 그리고 문제가 생겼을 때 어떻게 해결해 나갈지에 대한 값진 인사이트를 얻었다. 이렇게 얻은 인사이트들과 그동안 작성한 회고를 돌아보며 다음 프로젝트 때는 똑같은 문제와 실수를 반복하지 않도록 확실하게 규칙과 환경을 조성할 수 있게 된 것 같다. 많은 것을 얻어가는 좋은 프로젝트 경험이었다.