React 박치기

김태성·2024년 4월 29일

내실 다지기

목록 보기
8/11

될때까지 박치기 하면서 배운거 적는 공간

데이터에 null, trash 값 섞임

문제상황

백엔드 서버로 post 하는 데이터에 null, trash 값이 섞임.

객관식 : [문제, 선택지1, 선택지2, 선택지2, 선택지3, 해설]
주관식 : [문제, 답, 해설, null, null, null]
이렇게 데이터가 들어감.

데이터를 주는 사람이야 상관은 없는데
팀원분이 데이터베이스에서 null값이 들어가면 프레임워크가 터진다고 한다.(무섭다)

원인

  handleAnswerChange = (index: number, value: string) => {
    const newAnswers = this.state.answers.map((answer, idx) =>
      idx === index ? { ...answer, text: value } : answer
    );
    this.setState({ answers: newAnswers });
    this.props.updateAnswer(index, newAnswers);
  };


  handleSelectionChange = (index: number) => {
    const newAnswers = this.state.answers.map((answer, idx) =>
      idx === index ? { ...answer, selected: !answer.selected } : answer
    );
    this.setState({ answers: newAnswers });
    this.props.updateAnswer(index,newAnswers);
  };

데이터를 관리하는 방법이 잘못되었다.
그저 데이터가 들어왔을때 바로 nesAnsers에 업데이트를 해버리고,
배열 크기를 6으로 고정해놓고 쓰니 주관식도 배열의 크기가 6이 되버린다.

심지어 위의 코드는 새로운 데이터가 들어가면 새로운 배열이 생기는
잘못된 코드이다.(지금은 fix 했다.)

해결방법

  handleTypeChange = (e: ChangeEvent<HTMLSelectElement>) => {
    const newType = e.target.value as 'objective' | 'subjective';
    const initialAnswers = newType === 'objective' ? 
    [{ text: '' }, { text: '', selected: true }, { text: '' , selected: true }, { text: '', selected: true }, { text: '', selected: true }, { text: '' }] :
      [{ text: '' }, { text: '', selected: true }, { text: '' }];

    this.setState({
      questionType: newType,
      answers: initialAnswers
    }, () => {
      this.props.updateAnswer(this.props.id, initialAnswers);
    });
  };

주관식/객관식 선택지를 변경할때 배열을 바꿔버린다.
하드코딩의 느낌이 솔솔 나긴 하지만 현제로써는 최선인거 같다.
위의 코드를 짧게 설명하자면

  • 객관식일때는 배열을 6개로, 주관식일때는 3개로 초기화 한다.
  • 내부 데이터는 모두 null값으로 초기화 시킨다.

힘들었던 이유

예외상황 처리에 대해 미숙했다. 데이터를 다뤄본적도 없었고,
구현을 목적으로 빨리빨리 하다보니 객관식 답을 체우고 주관식으로 바꿨을때
동적으로 업데이트된 데이터가 남아있다는걸 깜빡했었다.

프로젝트가 끝나고 보충

<QuestionComponent
              key={component.id}
              id={component.id}
              expand={component.expanded}
              onToggle={this.toggleExpand}
              onDelete={this.deleteQuestion}
              answers={this.state.answers[index]}
              updateAnswer={(index, newAnswers) => this.updateAnswers(index, newAnswers)}
              updateTime={(index, timeExchange) => this.updateQuestionTime(index, timeExchange)}
            />

문제 만드는 요소를 컴포넌트화 시켜서 데이터를 받아내는 형식으로 바꿨다.
이렇게되면 좋은것이 각각의 요소를 따로 관리할 수도 있고, 나중에 바꾸기도 편해진다.
위에서 나타났던 null값이 들어오는 문제 또한
그냥 answer에 정보가 입력된 것만 배열로 받으면 된다.

데이터 형식 변환

문제 상황
데이터를 보내는데 자꾸 error가 뜬다.

원인
위의 상황과 거의 맞물리는데, 웹사이트를 코딩하고 입력받기 편하게 구조를 짜다 보니 데이터베이스에서 요구하는 데이터 구조에 맞지 않았음.
팀프로젝트가 처음이다 보니 요구사항을 보는게 처음이어서
재대로 하지 못했던거 같다.

데이터베이스에서 원하는 데이터 구조
title(문제집명), 
subLectureUrl(강의 url), 
subLectureTitle(소강의명),
mainLectureTitle(대강의명), 
lecturerName(강사명),
duration(강의시간, 초단위 정수) ,
[
instruction(문제), commentary(해설), 
popupTime(팝업시각, 'hh:mm:ss'), 
[content(선택지), isAnswer(정답여부)]
 ]


내가 보내던 자료구조

title: title,
videoUrl: videoUrl,
answers: answers,
time:questionTimes,

해결방법
보내기 전에 구조에 맞게 데이터 구조를 뜯어고친다.

quizzes라는 const값으로 묶어버릴 것이다.
answers는 3개 혹은 6개의 원소를 가진 배열인데
이 중 0번과 -1번은 각각 문제, 해설이 된다.
그리고 나머지 중간 배열을 commentary로 묶어버린다.
이후 popuptime이라는 문제 시간도 함께 묶는다.

    const quizzes = this.state.answers.map((answerSet, index) => ({
      instruction: answerSet[0].text,
      commentary: answerSet[answerSet.length - 1].text,
      popupTime: this.state.questionTimes[index],
      answers: answerSet.slice(1).map((answer, idx) => ({
        content: answer.text,
        isAnswer: answer.selected
      })

이렇게 quizzes로 문제/해설/팝업시각/선택지(지문, 정답여부)를 구조에 맞게 보낼 수 있었다.

또한 필드가 비었을때 데이터를 못보내게 했고, 시간의 형식도 확인했으며 문제에 공백이 있으면 못보내게 하였다.

// 필드 확인 함수
    const { title, subLectureUrl, mainLectureTitle, subLectureTitle, lecturerName, duration } = this.state;
    if (!title || !subLectureUrl || !mainLectureTitle || !subLectureTitle || !lecturerName || !duration) {
      console.error("모든 필드를 채워주세요.");
      return;
    }
//시간 조건 확인 함수
  const timeRegex = /^(2[0-3]|[01]?[0-9]):([0-5]?[0-9]):([0-5]?[0-9])$/;
  if (!timeRegex.test(this.state.questionTimes[index])) {
    throw new Error(`시간 형식이 잘못되었습니다. 'hh:mm:ss' 형식으로 입력해주세요. (문제 ${index + 1})`);
  }
// 문제 공백 확인 함수
  if (answerSet.some(answer => answer.text === '')) {
    throw new Error(`answerSet에 빈 값이 포함되어 있습니다. (문제 ${index + 1})`);
  }

마지막 answerSet은 answer함수를 순회하며 answer.text의 값이 ' ' 일때 error를 발생시킨다.

구글 로그인 에러

문제상황

팀원이 clone한 프론트 클라이언트에서 로그인이 안된다고 했다.

원인

메시지를 확인해보니 로그인 권한이 없다는 것이었다.

구글 로그인은 구글과 데이터 통신을 할때 특정 ip를 기반으로 포트를 뚧어야 한다. 이때 localhost도 지원하는데, 내가 사용하는 포트번호만 뚧었고 팀원의 포트번호가 안뚤려 있었다.

google oauth api 관리에서 localhost:3000만 뚧어놨는데
팀원은 백엔드 개발을 하던 중이라 포트번호가 3002번이었다.

해결방법

포트를 더 뚧었다.

부모 <-> 자식 컴포넌트 데이터 통신

문제상황

  • 자식 컴포넌트에서 부모 컴포넌트로 데이터를 옮기고 싶음.

원인

  • 부모 컴포넌트에서 자식으로 데이터를 내려보낼때는 그냥 보내면 끝이었지만 자식에서 부모를 보낼때, 보내는 방법을 찾지 못함.

해결방법

  • 부모 컴포넌트에서 상태 변수를 하나 만들어서 자식 컴포넌트에 내려준 뒤, 자식컴포넌트에서 이 변수를 변경함. 동적 변경이 되어서 부모 컴포넌트에서 사용 가능해짐.

힘들었던 이유

  • React도 첨쓰는데 Typescript도 같이 쓸려고 하니 타입 설정이 힘듬
  • 다른 컴포넌트와 동적으로 연결되는 방법을 몰랐음.

부모 컴포넌트 코드

import React from 'react';

const ProblemPage: React.FC = () => {
  const [answers, setAnswers] = useState<string[][]>([]);
  // 자식 컴포넌트로 내려줄 상태변수
  
  
   const updateAnswers = (index: number, newAnswers: string[]) => {
    setAnswers(answers => {
      const updatedAnswers = [...answers];
      updatedAnswers[index] = newAnswers;
      return updatedAnswers;
      // 상태변수를 자식 컴포넌트에게 내려주기 위한 코드.
      // 지피티는 함수형 업데이트라고 한다.
    }); 
  
 ~~~
    
    <QuestionComponent
              updateAnswer={(newAnswers) => updateAnswers(index, newAnswers)}
            />
  		// 자식컴포넌트에게 내려주는 함수
~~~

자식 컴포넌트 코드

const QuestionComponent: React.FC<{ ~~ updateAnswer:(newAnswers:string[])=>void }> = 
({ ~~ updateAnswer  }) => { 
  	//	부모 컴포넌트에게서 받은 함수
  
  
      const handleAnswerChange = (index: number, value: string) => {
      const newAnswers = answers.slice();
      newAnswers[index] = value;
      updateAnswer(newAnswers); 
    };
return ~~

}

쿠키 사용 방법

우리 서버는 기본적인 jwt 토큰을 사용한다.
복잡한건 아니고 다음과 같은 로직을 가진다.

문제상황

  • 쿠키를 쓰고싶은데 방법을 모름.

원인

  • get, post 차이점 미숙지, 백엔드와 협의를 안함.

해결방법

  • 쿠키사용법 공부, 백엔드와 함께 코딩, 의논하면서 서로가 데이터를 잘 받고 보낼 수 있게 함께 고민하고 코딩하는 시간을 좀 길게 가졌음. 결국 다 고치고 정상작동됨.

힘들었던 이유

  • 로그인 기본 로직은 내가 만든것도 아니라서 이해도 못했었고(그렇다고 내가 다 짰으면 시간 더걸렸을듯), 프론트 css랑 홈페이지 구조도 혼자서 다 짜고 있어서 시간부족, input 데이터를 가져오는게 힘들었음.

코드

// 로그인 코드
// 구글에서 로그인 정보를 받아와서 서버로 보낸 후, return 값에 따라서 회원가입
// 페이지로 갈지, 쿠키 제작 후 workbook 페이지로 이동할지 판단.
localStorage.setItem('token',JSON.stringify(userToken));
      interface UserToken {
        credential: string;
      }
      const userTokenString: string | null = localStorage.getItem('token');
      let credential: string;
      if (userTokenString) {
        const userToken: UserToken = JSON.parse(userTokenString);
       credential = userToken.credential;
     }else{return;}
      
      const response = await axios.get('http://192.168.0.143:3000/api/member/signin', {
        headers: {
          'Authorization': `Bearer ${credential}`
        },
      });
      
      
      if (response.data === '') {
        navigate('/signup');
      } else {
        localStorage.removeItem('token')
        document.cookie = `token=${response.data.token}; expires=${response.data.expire}`;
        navigate('/workbook');
// 회원가입 페이지도 위와 유사한 기능을 가진다.
// 값을 post하고, jwt가 return된다면 쿠키를 만들고 이동, 아니라면 그대로
        
//문제를 만든후 서버로 요청하는 페이지.
const cookie = document.cookie.match('(^|;)\\s*' + 'token' + '\\s*=\\s*([^;]+)');
const token = cookie? cookie.pop():''; // 원하는 쿠키 찾기
    try {
      const response = await fetch('http://192.168.0.143:3000/api/quizsets', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${token}`, 
          // `랑 ' 햇깔리지 않기
          // 헤더에 토큰을 넣어서 보낸다.
        },
        body: JSON.stringify({
          title: title,
          videoUrl: videoUrl,
          answers: answers,
          time:questionTimes,
          //body는 내가 보낼 데이터들
        }),
      });

      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
		// response 확인 코드. 아직 서버에서 코드가 만들어지지 않음.
      	// return 받은 token으로 쿠키를 갱신한 후, workbook
      	// 페이지로 이동할 예정.
      const responseData = await response.json();
      console.log('Server Response:', responseData);
    } catch (error) {
      console.error('Error:', error);
    }

기본적으로 쿠키를 사용한 통신은 post를 사용하고 있다.
헤더에 authorization 정보를 넣을 수 있어서 서버에서 편하다고 한다.
쿠키는 헤더에, 데이터는 바디에 넣고 있으며 post 요청마다 쿠키를 발송하고
return 된 token과 expire 값으로 쿠키를 갱신한다.
보안을 위해서 cookie의 지속시간을 너무 짧지는 않지만 길지도 않게 해놨다.

라고 끝내면 원숭이가 되는거니까 get과 post에 대해서 알아봤다.

Get

  • GET 메소드는 데이터를 URL에 질의 문자열(query string)로 포함시켜 전송한다. 이는 URL이 로그에 기록되거나, 브라우저 히스토리에 저장되고, 북마크될 수 있다

  • 따라서 민감한 정보(예: 비밀번호, 개인 식별 정보 등)를 GET을 통해 전송하는 것은 매우 위험할 수 있다.

Post

  • POST 메소드는 데이터를 HTTP 메시지 바디에 포함하여 전송한다. URL에 데이터가 노출되지 않기 때문에 GET에 비해 상대적으로 안전하다.

  • 즉 POST 요청은 일반적으로 서버 로그에 데이터 내용이 기록되지 않으며, 캐시도 되지 않는다.

한줄 요약하면 민감한 개인정보 데이터는 post로, 털려도 되는 데이터면 get으로 보내라. 라고 한다.

하지만 하나 더 고민이 되는게.. 데이터는 어차피 털리면 해커가 다 가져간다고 보면된다. 그런데 왜 굳이 이렇게하는가? 라고 보니

Get은 리소스를 요청하기 위해서 사용하는것이고, Post는 리소스를 업데이트/생성하기 위해서 사용한다.
GET은 Idempotent, POST는 Non-idempotent 하게 설계되었다.
라고 한다. 멱등성이라고도 한다.

생성에는 POST, 수정은 PUT 또는 PATCH, 삭제는 DELETE를 쓰라고 한다. 더 알아보고싶은데 시간이 없다;


**프로젝트가 끝나고 보충**
export const getAuthToken = () => {
    const cookies = new Cookies(); 
    let token = cookies.get('jwt');
    if(!token){
        token = null;
    }
    return token;
}

export const request = (method, url, data) => {
    let headers = {};

    if (getAuthToken('jwt') !== null && getAuthToken('jwt') !== "null"){
        headers = {"Authorization" : `Bearer ${getAuthToken('jwt')}`};
    }

    return axios({
        method : method,
        headers : headers,
        url : url,
        data : data
    })
}

인증도 컴포넌트화 시켜버렸다.
localstorage로 쿠키를 보내니 뭐니 복잡하고 위험한 방식은 싹다 걷어내버리고,
쿠키를 재대로 만들어서 통신할때마다 header에 쿠키를 넣어 보내는 방식으로 바꿨다.

데이터 반환구조 변경 및 리팩토링


const ProblemPage: React.FC = () => {
  const [videoUrl, setVideoUrl] = useState<string>('');
  const [title, setTitle] = useState<string>('');
  const [questionComponents, setQuestionComponents] = useState<{ id: number, expanded: boolean}[]>([]);
  const [answers, setAnswers] = useState<{ text: string, selected: boolean }[][]>([]);
  const [questionTimes, setQuestionTimes] = useState<string[]>([]);
  
  const QuestionComponent: React.FC<{ id: number, onToggle: (id: number) => void, onDelete: (id: number) => void, expand: boolean, answers: { text: string, selected: boolean }[], updateAnswer: (newAnswers: { text: string, selected: boolean }[]) => void , updateTime: (index: number, newTime: string) => void}> =
  ({ id, expand, onToggle, onDelete, answers, updateAnswer, updateTime }) => {
    
    const [questionType, setQuestionType] = useState<'objective' | 'subjective'>('objective');
    const [newTime, setTime] = useState('');


const addQuestionComponent = () => {
  };

  const updateQuestionTime = (index: number, timeExchange: string) => {

  };

  const updateAnswers = (index: number, newAnswers: { text: string, selected: boolean }[]) => {
  };

  const toggleExpand = (id: number) => {
  };

  const deleteQuestion = (id: number) => {
  };

	// 각각 적어도 5줄 이상, 쓰이는방식도 재멋대로

문제상황
코드도 더럽고 백엔드 서버로 보내는 자료형도 잘못 보내고 있다.
비효율적이고 가독성 떨어지는 코드들이 한가득이고 일명 '스파게티 코드'이다.
지속적으로 유지보수를 하고 백엔드로 정확하게 데이터를 보내기 위해서는 refactoring을 해야 한다.

원인
리액트가 처음이라 공부도 하고 일단 사이트에서 데이터가 재대로 들어오는지,
그리고 post요청이 재대로 가는지 확인하기 위해서 일단 코딩하고 봤음.
하지만 앞으로 1달가까이 볼 코드들이라 구조를 뜯어고쳐야됨.

해결방법
react class를 사용해서 요청데이터를 자식 컴포넌트에서 하나의 데이터로 묶은
후 부모 데이터로 올릴 예정임. 이때 데이터의 형식은 백엔드 서버로 바로 보낼 수
있는 형태로.

힘들었던 이유
다 끝나고 나서 알게된건데 class는 방식도 복잡하고 구식이라고 한다.
특히나 Hook도 사용하지 못해서 래핑 후 props를 건내줬는데, 요즘은 다 function을 쓴다고 한다.
리팩토링을 한번 더 해야한다.

new Error 발생

문제상황

new Error에서 alert -> 로직 중단으로 리팩토링 하고 싶음.

바꾸기 힘들었던 이유
alert는 단순히 경고창을 띄워주는 용도고 프로세스의 진행 자체를 막는건 불가능하다.
그래서 추가적인 로직을 넣어서 alert가 뜰때 진행을 막는 로직을 추가해야한다.

해결방법

    if (!title || !subLectureUrl || !mainLectureTitle || !subLectureTitle || !lecturerName || !duration) {
      alert('모든 필드를 채워주세요.');
      return;
    }
//모든 필드를 채크해서 만약 요구조건에 맞지 않다면 return을 함으로써 void 데이터를 전달.

      if (!response.ok) {
        switch(response.status){
          case 412:
            alert('same title');
            break;
            // throw new Error('same title');
          default:
            alert('Network response was not ok')
            break;
            // throw new Error('Network response was not ok');
      }
// 서버는 특정한 조건에 맞지 않는 데이터를 수신한다면 412에러를 발생.
// 정확한 데이터가 아니면 데이터베이스에 접근 불가.

백엔드 서버 로컬로 하나 더 열기

문제상황
백엔드도 작업을 해야 되서 로컬로 clone 받은 백엔드를 로컬에서 열었음.

해결 과정

  1. env 를 install 해서 로컬 react 서버를 3002번 포트로, nest 서버를 3000번 포트로 열었음.

  2. ip 주소로 보내던 요청을 localhost:3000으로 바꿈

  3. npm run start:dev로 팀 프로젝트 데이터베이스에 연결함

  4. 이후 console로 확인을 하며 정상작동되는지 점검

힘들었던 이유
다른사람의 코드가 어떻게 작동하는지를 정확하게 파악하지 못해서
데이터가 들어오지 않는게 다른 브랜치를 clone 한 것인지, 아니면 주소 연결이 잘못되었는지, 그것도 아니면 데이터베이스가 작동을 안하는건지 파악하는것이 힘들었음.

꼼꼼히 살펴보며 console을 찍음.

사이트 ui 개선

문제상황
팀장님한테 사이트가 게시판같다는 클레임이 들어왔다.

원인

사이트가 너무 정적이다. 커서에 컴포넌트가 닿아도 반응형이 하나 없고 동작도 에니메이션이 하나도 없다.

해결방법

라이브러리를 사용했다.

이것에 대한 정리를 추후 따로 적을 예정.
추가 공부 필요

프로젝트가 끝나고 보충

강의를 듣다가 문제가 만들고 싶을때 키는 창이다.
전체적인 크기의 비율도 맞추고, 배경을 넣고
의미없는 구역화를 없애버렸다.
위에 보이는 페이지는 화면의 절반정도 크기를 가지고 있는데,
가로로 너무 길어지면 오히려 밋밋하게 보일 수 있고 시선의 움직임이 커져
유저가 불편할 수도 있다는 것을 알았다.

params

문제상황
데이터를 자식 컴포넌트로 보내고 싶은데 간결하게 하고 싶음.

방법
useParams를 사용했는데 순서가 있음.

  1. 라우팅 할때 /:(text) 형태로 바꿔야됨
<Route path="/question_info/:quizSetId" element={<Question_info/>}/>
  1. 데이터를 보낼때 url에 담아서 보내야됨.
navigate(`/question_info/${quizSetId}`,{ state: { subLectureUrl } });
  1. url을 통해서 데이터를 받아들임
const {quizSetId} = useParams();

// 받아들여지는 데이터 : /:quizSetId의 quizSetId값
//이경우 게시물의 id 번호가 나옴

location 객체

위에서 계속 쓰고 있는 컴포넌트 간 데이터 통신에서
부모 컴포넌트에서 자식 컴포넌트로 데이터를 옮기는 과정이다.

방법
navigate를 사용해서 데이터를 보낼때 state:에 데이터를 함께 보낸다.

    navigate(`/question_info/${quizSetId}`,{ state: { subLectureUrl } });
//부모 컴포넌트에서 자식 컴포넌트로 네비게이션을 쓴다.

그러면 자식 컴포넌트에서 location을 보면 데이터가 보인다.

  const location = useLocation();
  console.log(location);


데이터가 잘 담겨있는 모습을 볼 수 있다.

세션이란?

쿠키는 해킹당했을때 개인정보가 유출된다는 취약점이 있다.
그래서 세션은 개인정보를 서버에서 저장하는 형식이다.

쿠키의 흐름을 생각해보면

  • 로그인한다
  • 서버가 쿠키를 클라이언트에게 준다.
  • 클라이언트는 통신할때마다 쿠키를 서버에 제출한다.
  • 올바른 쿠키라면(변조X) 통신이 잘 된다.

위와 같은 흐름을 가질 것이다.
세션도 위와 비슷하지만 좀 다르다.

  • 로그인한다.
  • 클라이언트는 쿠키 대신 세션ID를 얻고, 서버가 쿠키를 가진다.
  • 클라이언트가 통신할때 이 세션ID를 같이 제출한다.
  • 서버는 가지고있는 유저 정보를 세션ID로 식별해서 이용한다.
  • 처리 후 데이터를 클라이언트에게 전해준다.

세션과 쿠키의 차이점은 꽤나 간단하다.
쿠키는 개인정보를 계속 통신에 써야하고
세션은 쿠키를 서버가 가지고 있어야 한다.

그래서 쿠키는 취약점이 있는 대신 가볍고
세션은 취약점을 보완했지만 쿠키가 쌓이면 느려진다.

profile
닭이 되고싶은 병아리

0개의 댓글