본문 바로가기

Projects/2024

[ Project4 / 푸딩캠프 ] 컨퍼런스-언컨퍼런스 (Feat. 생각나는 기술회고)

이번 컨퍼런스 홈페이지를 제작하면서 여러가지 기술을 사용해 봤다. 나름 흥미있었고 진담을 뺐다. 왜냐하면 실제로 사용자가 내가 만든 서비스를 사용해 봤다는 것이기 때문이다. 물론 사용자로부터 실질적인 피드백을 받지 못했지만 그래도 천리길도 한 걸음씩이다.


 

페이지를 만들면서 만났던 과제를 적어내려가 보려 한다.

1. 결제 페이지 제작

2. 스케줄러 제작

3. Tab Button 별 URL만들기

 


 

개발 환경(Client)

언어 : React Js + Typescript
Framework: Vite
라이브러리 : react-router-dom, zod, tosspayments-sdk(ver2)
PG사 : 토스

 


 

1. 결제 페이지 제작

결제 페이지를 만들면서 직면한 과제는 크게 3가지 인거 같다.

  1. 결제 페이지에서 CORS 오류 이슈
  2. 가볍게 백이랑 데이터 형태 맞추는 이슈
  3. 팝업으로 띄우는 작업

 

결제 세팅

결제 페이지는 푸딩캠프에서 이미 가지고 있는 토스 페이지를 사용하기로 했다. 결제 페이지를 한 번도 만들어 본적이 없었기 때문에 어떻게 청사진(코드)을 그려 나가는지 몰랐기 때문이다.

결제 페이지를 제작하는 방식을 몰라서 토스 sandbox 있는거 그대로 가져왔다.

  1. 토스페이지가서 sandbox에 있는 내용을 그대로 활용하여 제작한다.
  2. sandbox에 있는 코드를 그대로 사용한다.

 

토스 프로세스

토스페이먼트 SDK version2를 사용.

언어 Front : React js | Back : Node.js

결제 페이지 플로우 :  결제 페이지 들어감 -> 결제를 위해 신청 입력폼 작성 ->  '결제' 버튼 클릭 -> <TossPayment > component 팝업 출력 -> Toss 페이 결제 진행 ->  '결제하기' 버튼 클릭 ->  a) 결제 완료시 -> a-1) 결제 완료 페이지 -> b) 결제 실패 -> b-1) 결제 실패 페이지

//결제 버튼이 누르면 <TossPayment > component가 팝업으로 띄워지게 만듬.

결제 신청 페이지
const Payment = () => {
  const [ tossValues, setTossValues ] = useState();
...
  return(
...
  **form tag** 영역
...

   {/* useState hook에 데이터가 들어오면 출력 */}
  {
    tossValues &&
    <TossPayment
        personData={tossValues as PersonData}
        isClicked={isClicked}   // Payment component의 submit(결제) 버튼을 누르면 <TossPayment > 파업이 뜸.
        setIsClicked={setIsClicked} // 팝업이 뜬 걸 닫기 위해서 set- 함께 전달.
    />
  }

)

 

1.결제 페이지에서 CORS 오류 이슈 : 첫 handshake

interstella'

과제

당연히도, 뒷단에서 나의 IP를 열어 줬지만 통신이 되지 않는다.

해결(?)

프론트인 내가 할 수 있는 건, 보낼 때 이런 저런 걸 덕지 덕지 보내는 것.

// front end가 CORS를 해결하는 방법...

const submitData = {
  method: "POST",
  cache: "no-cache" as RequestCache, // 이런 걸 추가
   credentials: "same-origin" as RequestCredentials, // 이런 걸 추가
   headers: {
     "Content-Type":"application/json",
   },

사실이거 해결하는데 2시간 넘게 걸렸다. 보내고 확인하고 보내고 확인하고....

여기서 내가 할 수 있는 일은 위에 있는 거 밖에 없다.

궁금증

왜? 뒷단에서 IP 허용을 해줬는데 안/못 들어가는가 이다....

 

2. 가볍게 백이랑 데이터 형태 맞추는 이슈

처음 백(back-end)이랑 데이터를 주고 받는 걸 해봤다. 사실 혼자 할 땐, 당연하다고 생각하는 부분을 누군가 문서를 작성해서 전달받고 해보는 경험이 생소해서 기억이 남는다... 이번 계기를 통해서 뒷쪽에 보내는 데이터의 값을 동일(key and data type)하게 보내지 못하면 데이터가 맞지 않는다고 반응값(response)을 보내온다는 사실을 알았고 그걸 처리하는 로직이 필요하다는 사실을 새롭게 습득했다.

//뒤에서 만든 데이터 전달 형태에 따른 결과 값

201 Created: 새로운 결제 정보가 성공적으로 생성됨
{
  success: true,
  "message": "결제 내역이 저장되었습니다.",
  "userId": string,
  "paymentId": string
}

400 Bad Request: 잘못된 요청 데이터
{
  "error": "Invalid data provided",
  "message": string
}

409 Conflict :
{
  success: false,
  message: '이미 오프라인 결제 내역이 있는 사용자입니다.',
}

500 Internal Server Error: 서버 오류 / 트랜잭션 롤백
{
  success: false,
  message: 'An unexpected error occurred',
}

 

다행이 몇 번 주고 받고 나서 어떻게 하는 지 알고 형태와 타입을 맞췄다.

추가적으로 더 배우게 된 사실은 response 가 ok가 아닐 때, 반응이다.

// client 에서 response 핸드링

try{
      const userEmail = cleanedData.email;
      const response = await fetch('https://puddingcamp.com~~~', submitData)

      if(!response.ok){
        if(response.status === 400){
          throw new Error(`입력정보 값 확인을 해주세요.`);
        }
        if(response.status === 409){
          throw new Error(`이미 존재하는 핸드폰 번호입니다.`);
        }
        if(response.status === 500){
          navigate('/errors/500');
          return
        }

        throw new Error("오류가 발생했습니다.")
      }

사실 이 방식이 맞는지는 모르겠다. 다만, 이런 것도 준비를 해야 한다는 사실을 인지할 수 있었던 계기가 됐다.

 

3. <TossPayment /> 팝업으로 띄우는 작업

<TossPayment >를 별도 component로 만들어서 useState Boolean을 통해서 랜더링 시키도록 했다. 물론 그냥 페이지에 최하단에 노출 시킬 수 있었지만 총괄의 요청으로 페이지를 띄우기로 했다.

conference-unconference, 푸딩캠프

Toss 위젯을 띄우는 순서

  1. 최초 client에서 form 정보를 back으로 보낸다.
  2. back에 정보가 올바르게 들어오면 uid를 생성해 uid 정보를 client로 보낸다.
  3. uid가 useState에 채워지면 boolean으로 { tossValues(true) && <TossPayment /> } 가 활성화 된다.
  4. <TossPayment >를 둘러싼 <div>를 통해 position : absolute로 띄운다.

 

과제

<TossPayment >component는 토스 페이지 sandbox의 내용을 그대로 가져와 붙였기 때문에 어떻게 띄워야 하지 몰랐다. 기존 코드에 class를 넣고 style을 적용해 보았지만 적용되지 않았다.

해결

총괄에게 조언을 듣고 <div>로 <TossPayment > 즉, 토스  sandbox에서 복제해 온 걸 감싸고 전체를 띄웠다. 더불어 사이즈를 조절했다. <TossPayment > component를 별도로 만들었기 때문에 props로 isClicked와 setIsClicked을 함께 전달해야만 나중에 닫기 버튼을 눌렀을 시 팝업을 닫을 수 있었다.

궁금증

다만, TossPayment의 세부사항의 style을 조정할 수 없었다. 위젯(?)의 너비 정보만 조절이 가능했다. 이건 내가 정보를 몰라서 그런 것일까? 지금도 궁금하다.

 

추가 확장 : 위젯을 띄워야 할까? 아니면 page 하단에서 처리해야 할까?

이건 더와 덜도 차이를 모르겠다.

확인을 위해 애플나이키 페이지의 결제 페이지를 확인했다.

 

 


애플 : 페이지 이동을 시켜 결제 페이지로 간다.(URL 값이 변경됨)

애플


 

나이키 : 팝업으로 출력

나이키(ko)

 

나이키는 같은 페이지에 아코디언 형식으로 주소, 결제 페이지를 부분적으로 저장 '결제하기'를 하면 웨젯이 뜬다.

 

결국 차이점은?

두 가지 사례로 결론 내리긴 어렵지만, 기획자 스타일이지 않을까 한다.

 

 

 

2. 스케줄러 돌리기 (Feat. setInterval)

'스케줄는 왜 만드는 가?' 라는 질문에서 시작해 보면, 온라인 신청한 사람들에게 메일로 통해서 URL을 보내지만 혹시 모를 상황을 대비하여 홈페이지 메인에 생방송 버튼을 출력하기로 했다.

 

과제 : 특정 시간에 버튼이 출력되게 해줘.

내가 스케줄로를 만든 이유는 컨퍼런스가 시작하면 라이브 방송 버튼이 노출돼야 했다.

 

어떻게 만들것 인가?

콜백 함수를 이용할 수 있는 JavaScript method

method 동작  
setTimeout 대기시간 이후 콜백 함수 호출
(한번만 동작)
setTimeout(func | code, [delay], [arg1], [arg2], ...)

첫번째 : 함수 또는 code
둘째 : 지연 시간
세번째 :콜백함수에 전달 인수
setInterval 대기시간 마다 콜백 함수 호출
(주기적 반복 동작)
setInterval(func | code, [delay], [arg1], [arg2], ...)

첫번째 : 함수 또는 code
둘째 : 지연 시간
세번째 :콜백함수에 전달 인수

참고 글 : https://url.kr/oecq48

활용할 방식은 브라우저가 랜더링을 할 때, 변수에 현재 시간을 찍고 이 찍은 시간과 1초마다 확인을 해서 시간이 넘었는 지 넘지 않았는 지를 확인했다.

CHAT Ai의 도움을 받아 아래와 같이 code를 작성했다.

const [ isOpeningTime, setIsOpeningTime ] = useState(false)


useEffect (()=>{
  const targetTime = new Date("2024-09-28T13:00:00"); // 이 시간에 되면 함수를 호출해
  let intervalId : Node.js.Timeout // 타입을 넣는 것이라고 Ai가 말해 줌. + 브라우저 내부에 setInterval 고유 아이디도 함께 저장 됨(아래 추가 설명).
 
  const checkTime = () =>{
     const now = new Date();

     if(now >= targetTime){
        setIsOpeningTime(true);
        clearInterval(intervalId); //내부에 저장한 setInterval 을 Id 완전한(?) 삭제.
    } 
  };

  checkTime(); // check time 실행
  intervalId = setInterval(checkTime, 1000); // interval 설정 및 id 저장. +  Web API를 통해서 setInteval 실행.


  return () => {
  if(intervalId) {
    clearInterval(intervalId)
}

},[ ]);

 

Javascript 엔진이 아닌 브라우저 엔진 Web API : setInterval

여기서 의아스러운 부분이 있을 수 있다. useEffect의 경우 의존성 배열이 비여있기 때문에 한 번만 실행되고 끝나야 하는 데, 어떻게 1초마다 체크를 할 수 있지? 라는 질문이다.

간단히 말하면, 작동하는 부서가 서로 다르다.

useEffect는 Javascript Engine이 실행하고 setInterval는 브라우의 WebAPI가 처리하기 때문에 setInterval이 한번 실행되면 브라우저가 지속적으로 관리 한다. 따라서 clearInterval 해줘야만 setInterval이 없어지기 때문이다.

 

 

 

3. Tab Button 별 URL만들기

사실 이건 이번에 해보면서 나도 피드백을 받은 부분이기도 하다. 생각도 못했던 부분이였기에 인상적이였다. tab button별로 발표자가 출력된다. 아래 보면 '성장홀'과 '도전홀'이 있다. 따라서 각 홀 마다 연자사자 다르다.

컨퍼런스 언커퍼런스by 푸딩

알다시피, tab button의 언제나 default값을 가지고 있다. 따라서 url을 공유해 주면 무조건 '성장홀'이 출력된다.

 

과제 : 각각의 tab button 구분하라

어떻게 Tab button을 따로 구분할 것인가?

동일한 데이터로 페이지를 한개 더 만들어서 2개의 페이지에 다른 디폴트 값을 넣을 수도 있다. 아니면 URL에 구분 값을 넣어 정보값을 구분한다.

// URL에 string 추가 하기

성장홀 : https://conference.puddingcamp.com/?tab=growth  // tab=grwoth 로 
도전홀 : https://conference.puddingcamp.com/?tab=challenge // tab=growth 로

 

어떻게? : useSearchParams + useEffect

const [ tab, setTab ] = useState(0);
const [ searchParams ] = useSearchParams();
const tabBtn = searchParams.get('tab');

useEffect (()=>{
  if(tabBtn === 'growth'){
    setTab(0)
  }
  if(tabBtn === 'challenge'){
    setTab(1)
  }
}, [ ])

 

이런 식으로 페이지가 로딩될 때, searchParams 값을 가지고 tab button 값을 설정하게 했다.

 

굳이 필요할까?

굳이 이게 필요할까라는 질문을 할 수 있도 있다. 굳이 라고 하면 해도 그만 안 해도 그만이다. 다만, 공유하고 한 번 더 tab button을 클릭해야 하는 상황과 더불어 공유하는 사람이 전달자에게 추가적인 설명을 해야 하는게 조금은 번거로울 거 같다는 생각에 이렇게 제작하게 됐다.

 

 

마무리 마음 정리

여러 가지 이슈들도 있었지만, 인상적이었던 것과 좀 더 사용자 중심적인 부분을 취합해서 기록해 보았다. 신입인 나로선 대단한 경험이지만 이미/벌써 이런 경험을 해 본 개발자라면 특별한 경험은 아닐지 모른다.

그럼에도, 누군가에 이러한 사소한(?) 기록이 도움이 되었으면 좋겠을 뿐만 아니라 나에게도 긍정적인 피드백이 왔으면 좋겠다는 생각을 해본다.

마지막으로 이 프로젝트를 진행하면서 design pattern에 필요성에 한 발짝 내딛게 됐다. 변수가 은근히 않은 프로젝트였기에 그것 또한 컨트롤하고 싶은 나의 바람이 들어가 있다.

여전히 막막한 취업시장이다.

업계에 온전히 몸담고 조금 더 앞으로 나아가고 싶은 희망을 가져본다.