archive/Project

[MainProject] day-14,15 (5/18-5/19)

creatorAron 2023. 5. 19. 19:30
오늘 할 일

1. 자잘한 버그들 fix 및 게임등록 UI 수정,기능 수정 🐣

2. 검색 UI, 기능(구상만) 🪨

3. 메세지 UI, 기능

4. 지금까지의 작성한 코드의 지속적인 개선 🐣

5. 404페이지 🐣

 

 

오늘 오전 중의 활동은 특별히 기록할만한 부분이 없어 오후 1시 회의 이후 기록만 남깁니다😂😂😂

 

#1 버그 픽스 및 게임등록페이지 UI 1차 수정, 부가적인 기능 수정

팀원들과 회의 후에 게임등록 페이지가 일단 동작이되도록 만들었다..!!

jejus라는 이름으로 게임이 등록되었다. 사진이 현재 한 개만 등록 가능하도록 되어있는데, 여러 개 등록가능하도록 바꾸려다가 대표사진은 원래 하나만 등록되도록 구현하려고 했으므로 그대로 두기로 하였다. input 태그에 multiple 속성을 주고 onChange 핸들러에서 함수를 변경할 필요는 없어보이고 미리보기로 여러개가 등록되었다는 것을 보여주고 클릭으로 취소도 가능하도록 만드는 것이 필요하다.

 

**일단은 하나만 등록되도록 만들 것이지만 혹시 엣지케이스로 여러개가 보내질 경우 back-end에서는 어떻게 처리가 이루어지는지 궁금해서 물어보니, 랜덤으로 되는 것 같다고 말씀을 하셨다. 맨 처음 보내는 사진이 등록된다고 하셨다. 이건 내가 수정할 부분은 아닌데 작동이 잘 되는지 궁금한 부분이었다. 

 

게시글을 작성하는 과정에서 똑같이 파일을 등록하더라도 여러개를 등록할 수 있고 미리보기를 할 수 있는데, 게임채널 추가에서는 여러 장 등록은 필요없고 미리보기만 추가하면 될 것 같다.

 

1) 미리보기 추가

미리보기는 게시글 작성하는 팀원이 잘 만들어둬서 거의 그대로 사용할 것 같은데, 기존에 내가 검색해서 찾아본 방식과 동일하다. 작동되는 방식은 파일의 임시url을 생성하여 해당 url을 img 태그의 src 속성으로 지정해주는 것이다.

 

 

 

2) UI 1차 수정

antd를 제거하면서 틀어진 UI를 좀 수정하고 버튼 색 렌더링은 팀원이 오면 팀원이 랜덤으로 색 적용되도록 사용한 것을 그대로 가져와서 적용할 생각이다. 이미 만들어진 것을 적절하게 다시 사용하는 것도 능력이니까..ㅎㅎ

 

3) 자잘한 수정사항들

- 취소 이벤트 추가

navigation -1 로 뒤로가기 기능

 

- file input에 image로 파일유형 제한

 

#2 검색 UI

일단 팀원들이 사용한 컴포넌트들을 최대한 가져와서 활용하여 검색 결과 페이지 UI를 만들고, 검색 기능을 검색 바에 추가하도록 하겠다.

 

검색기능에 대해서 고민중인데, 현재 검색 힌트가 user: game: content: 이렇게 주어지는데 해당 힌트와 함께 검색하면 해당 결과만 노출되도록 하고 힌트입력없이 검색할경우 전체 검색결과가 노출되도록 할 것이다.

 

이 : 콜론을 기준으로 콜론이 포함되었을 경우 user: game: content: 을 찾아서 해당 힌트 뒷 부분을 검색하도록 하는 것이  1차적인 목표인데, 엣지케이스가 많이 발생할 수 있다. 힌트를 중복해서 사용하고 싶은 경우 어떻게 해야하는가?
case 1 : 'user: game: league of legend'
case 2 : 'user: league game: legend'

case 3 : 'user: '

이런 경우들을 어떻게 처리할지 미리 생각하고 로직을 짜면 훨씬 좋을 것 같다. 기본적으로 원하는대로 작동하는 것도 중요하지만 검색의 경우 엣지케이스가 워낙 많이 발생할 수 있으므로, 이런부분을 생각하는 것이 중요하게 느껴진다.

 

또 이런 케이스들도 충분히 발생할 수 있을 것 같아서 사실 드롭다운버튼을 넣을지 고민인데, 예를 들어

case 1 : 힌트를 적다가 틀린경우 - 'use: game', 'gamee: deongeon'

case 2 : 힌트뒤에 : 을 빼먹거나 ;를 입력한 경우 - 'user gamer', 'user; gamer'

이런 엣지케이스들을 한 번에 방지해주는 것이 힌트를 입력하고 싶은 경우에 드롭다운을 클릭해서 드롭다운을 선택해서 힌트를 적용하는 방법이다. 일단 가장먼저 모든 검색내용을 받아오는 것부터 완성하고 차근차근 필터를 추가하면서 드롭다운을 넣을지 고민하면 좋을 것 같다. 구체적인 작업내용은 주말 간에 계속해서 진행해보도록 하겠다.

 

#3 메세지 UI

 

#4 지금까지의 코드 developement

1) InputContainer는 끝나지 않았다!! useInput과 useValidation까지!!

(1) useInput

먼저 useInput 부터 변화를 살펴보자.

기존의 코드는 회원가입, 로그인 부분의 validation을 중심으로 다른 부분에서 사용될 input의 validation은 추가하려면 추가해서 써라 이런느낌이 강했다. 내가 재사용하기에도 별로 좋은 형태가 아니었다. 재사용성을 늘리고 이미 작성해놓은 validation은 Signup이나 Login 부분으로 옮겨서 코드를 클린하게 만들고 싶었다.

 

기존의 코드

import { useState } from 'react';
import { useDispatch } from 'react-redux';

import validation from './validation';
import { validationType } from '../types/utilTypes';

import { setSignupInfo } from '../slice/signupSlice';

/**
 * input 상태를 관리하는 커스텀 훅입니다.
 * @param {string} init - input 창의 초기값 (default: '')
 * @param {string} iputtype - input 창의 유형
 * @returns {Array} - input 상태, input 값 변경 시 핸들러, input 상태 변경함수, input 창의 유효성
 */
let data;

function useInput(init = '',validationType:validationType,customValidationFunction?:(value:string) => boolean) {
  const [value, setValue] = useState(init);
  const [validity, setValidity] = useState(false);

  const dispatch = useDispatch();

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    e.stopPropagation();
    setValue(e.target.value);
    setValidity(validation(validationType,e.target.value,customValidationFunction));

    //  Signup 관련 store 업데이트
    if(validationType === 'username') dispatch(setSignupInfo({key:'username', value:e.target.value}))
    if(validationType === 'email') dispatch(setSignupInfo({key:'email', value:e.target.value}))
    if(validationType === 'password') dispatch(setSignupInfo({key:'password', value:e.target.value}))
    console.log(validity);
  };
  data = { value, onChange , setValue, validity}
  return data;
}

export default useInput;

여기만 보면 validation 관련해서는 티가 안나는데 그 부분은 useValidation에서 설명하도록 하겠다. useValidation이 지금 코드상태에서 필요한 커스텀 훅인지는 아직 의문이 남아있기는하다.

Signup 관련 store 업데이트도 다른 input 들에서는 필요가없는데 포함되어있는 느낌이 강해서 추가적인 처리가 필요한 경우에 extraActionWithValue라는 인자로 e.target.value를 가지고 추가적인 작업을 할 경우 함수를 받아서 할 수 있도록해서 재사용성을 높였다.

 

수정된 코드

import { useState } from 'react';
import useValidation from './useValidation';

/**
 * input 상태를 관리하는 커스텀 훅입니다.
 * @param {string} init - input 창의 초기값 (default: '')
 * @param {function extraActionWithValue((params:string) {}:void)} - e.target.value를 store에 저장하는 등 e.target.value로 필요한 추가적인 액션을 하는 void 함수
 * @param {function customValidationFunction((params:string) {}:boolean)} - e.target.value의 유효성 검사를 진행할 함수
 * @returns {Object} - input value(상태), input에서 작동할 onChangehandler, input 창의 유효성검사 결과
 */

function useInput(init = '', extraActionWithValue?:(value:string)=>void ,
                  customValidationFunction?:| ((value: string) => boolean)
                                            | ((value: File) => boolean)) {
  const [value, setValue] = useState(init);
  const [validity, setValidity] = useState(false);

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // e.stopPropagation();
    setValue(e.target.value);
    if (customValidationFunction) setValidity(useValidation(e.target.value));
    if (extraActionWithValue) extraActionWithValue(e.target.value);
    console.log(validity);
  };

  return { value, onChange, validity}
}

export default useInput;

 

 

 

(2) useValidation

 

string과 File을 둘 다 validation 하는 경우를 고려해서 함수 인자의 타입을 : string | File 이렇게 설정하니 다음의 오류가 발생하였다. 해결법은!! & 연산자를 사용하는 것!! 리터럴끼리 연결하는 경우와 객체나 타입을 연결할 때 연산자가 다르게 작용하는 것을 항상 기억하고 활용하도록 해야한다.

No overload matches this call.
Overload 1 of 2, '(props: { title?: string | undefined; type?: string | undefined; placeholder?: string | undefined; onChange?: ChangeEvent<Element> | undefined; extraAction?: ((e: Event) => void) | undefined; validationMessage?: string | undefined; validationFunction?: ((value: string | File) => boolean) | undefined; } & { ...; } & { ...; }): ReactElement<...>', gave the following error.
Type '(value: string) => boolean' is not assignable to type '(value: string | File) => boolean'.
Types of parameters 'value' and 'value' are incompatible.
Type 'string | File' is not assignable to type 'string'.
Type 'File' is not assignable to type 'string'.

그래서 이렇게 해주면 해결된다.

// 수정 전
validationFunction?: (value:string | File) => boolean;

// 수정 후
validationFunction?: (value:string & File) => boolean;

아.. 해결이 된 줄 알았는데 수정 전과 수정 후 둘 다 적용이 안된다. 이래서 제대로 알고 써야한다..ㅋㅋㅋㅋ string | File로 준 경우 function 안에서 regEx에 string만 넣어줘야하는데 오류가 발생된다. 그럴 경우에 해결법은 있지만 File이 들어올 여지가 전혀없는데 아래와 같이 판단하는 로직을 넣는 것은 매우 비효율적이라 판단된다.

const usernameValidityTest = (value: string | File) => {
  if(typeof value === 'string') return usernameRegExp.test(value);
  return Error('Invalid input!')
};

그래서 파일만 들어오는 경우와 문자만 들어오는 경우를 각각 나눠서 두 경우가 다 될 수 있다고 처리해주니 오류가 사라졌다. GOOD!

결국 이렇게 최종 수정하였다.

validationFunction?:
    | ((value: string) => boolean)
    | ((value: File) => boolean);

 

그럼 useValidation의 변화를 살펴보면 다음과 같다.

 

수정 전

import { validationType } from '../types/utilTypes';

/**
 * 유효성 검사를하는 커스텀 훅입니다.
 * @param {string} inputtype - input 창의 type, username, email, password...etc로 구분.
 * @returns {Boolean} - validity:유효성 검사 통과했는지 여부
 */

const usernameRegExp = /^[가-힣A-Za-z0-9]{2,10}$/;
const emailRegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const passwordRegExp = /^[a-zA-Z0-9!@#$%^&*()_+-={}]{8,16}$/;


const usernameValidityTest = (value:string) => {
  return usernameRegExp.test(value);
}
const emailValidityTest = (value:string) => {
  return emailRegExp.test(value);
}
const passwordValidityTest = (value:string) => {
  return passwordRegExp.test(value);
}


function validation(validationType:validationType,value:string,customValidationFunction?:(value:string)=> boolean):boolean {
  if(validationType === 'username') {
    if(usernameValidityTest(value)) {
      return true;
    }
  }
  if(validationType === 'email') {
    if(emailValidityTest(value)) {
      return true;
    }
  }
  if(validationType === 'password') {
    if(passwordValidityTest(value)) {
      return true;
    }
  }
  if(validationType === 'custom') {
    if(customValidationFunction === undefined) return false;
    if(customValidationFunction(value)) {
      return true;
    }
  }
  if(validationType === 'none') {
    console.log('유효성검사가 없습니다')
  }

  return false;
}

export default validation;

 

수정 후

/**
 * 유효성 검사를하는 커스텀 훅입니다.
 * @param {string} value - input 창의 입력값.
 * @param {string} validationFunction - 유효성 검사하는 함수
 * @returns {Boolean} - validity:유효성 검사 통과했는지 여부
 */

function useValidation(value:string,validationFunction?:(value:string)=> boolean):boolean {
  if(validationFunction) return validationFunction(value);
  return false;
}

export default useValidation;

여기서 이 버그가 아직 남아있긴한데.. 이것을 어떻게 수정할지 아직 생각이 잘 안든다.

이 문제는 팀원들이 같이 해결해주기로 했다. 🥹🥹🥹(인공지능과 3명의 팀원이 합작해서 손봐도 좀 어려워서 일단 any type을 이용해서 응급처치 해두었다..ㅎㅎ)

타입스크립트에서 교차 타입 (intersection type)은 여러 타입을 결합하여 새로운 타입을 만드는 방법입니다. 그러나, 교차 타입의 동작 방식 때문에 발생하는 오류입니다. 이 경우, 타입 A는 string과 Object를 결합한 타입입니다.
typescripttype A = string & Object; let a: A = 'a'; // 작동함
이 경우, 문자열 'a'는 string 타입과 호환되며, 모든 문자열은 내장된 Object 타입과도 호환되기 때문에 오류가 발생하지 않습니다.
그러나 다음 예제에서는 오류가 발생합니다.
typescriptlet a: A = { a: 'b' }; // 오류 발생
문제는 교차 타입이 Object와 string 모두의 속성을 필요로 하는데, 이러한 조건을 만족하는 객체가 없기 때문입니다. 이 경우, {a: 'b'}는 Object 타입과 호환되지만, string 타입과는 호환되지 않기 때문에 오류가 발생합니다.
따라서, 해당 코드에서 오류가 발생하는 이유는 주어진 객체가 교차 타입 A에 정의된 모든 타입의 속성을 만족시키지 못하기 때문입니다.

 

(3) InputContainer와 그것에 prop을 내려주는 Signup/Login FieldsContainer

 

InputContainer를 살펴보기 전에 InputContainer에 prop을 내려주는 SignupFieldsContainer를 살펴보자.

 

수정 전

동료들과 머지를 해서 테스트를 하려다보니.. 수정 전 코드를 찾기가 너무 힘들어졌다. 폴더이동을 하는 바람에 아예새로만든 것으로 되어버려서 찾기가 힘들었으나.. 590개의 커밋을 뒤져서 다시 찾았다. 이런 것을 찾는 것도 한 번쯤(?) 해보아야 한다고 생각했다.

import styled from 'styled-components';

import InputContainer from '../../components/common/InputContainer';

const SignUpFieldsContainer = () => {
  return (
    <StyledSignUpFieldsContainer>
      <StyledInputContainer
        placeholder='ex)인디벗'
        title='닉네임'
        validationMessage='한글/영문 2-10자로 작성해주세요.'
        validationType='username'
      ></StyledInputContainer>
      <StyledInputContainer
        placeholder='ex)inddy@gmail.com'
        title='아이디'
        validationMessage='유효한 이메일 형식이 아닙니다.'
        validationType='email'
      ></StyledInputContainer>
      <StyledInputContainer
        placeholder='8자리 이상'
        title='비밀번호'
        validationMessage='비밀번호는 8-16자 영문,숫자,특수문자의 조합이어야 합니다.'
        validationType='password'
        type='password'
      ></StyledInputContainer>
    </StyledSignUpFieldsContainer>
  );
};

export default SignUpFieldsContainer;

const StyledSignUpFieldsContainer = styled.div`
  margin-bottom: 24px;
`;

const StyledInputContainer = styled(InputContainer)`
  margin-bottom: 24px;
`;

 

수정 후

import styled from 'styled-components';

import InputContainer from '../../components/common/InputContainer';

import { useDispatch } from 'react-redux';
import { setSignupInfo } from '../../slice/signupSlice';

const usernameRegExp = /^[가-힣A-Za-z0-9]{2,10}$/;
const emailRegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const passwordRegExp = /^[a-zA-Z0-9!@#$%^&*()_+-={}]{8,16}$/;

const usernameValidityTest = (value: string) => {
  return usernameRegExp.test(value);
};
const emailValidityTest = (value: string) => {
  return emailRegExp.test(value);
};
const passwordValidityTest = (value: string) => {
  return passwordRegExp.test(value);
};

const SignupFieldsContainer = () => {
  const dispatch = useDispatch();

  //  Signup 관련 store 업데이트
  const dispatchUsername = (value: string) =>
    dispatch(setSignupInfo({ key: 'username', value }));
  const dispatchEmail = (value: string) =>
    dispatch(setSignupInfo({ key: 'email', value }));
  const dispatchPassword = (value: string) =>
    dispatch(setSignupInfo({ key: 'password', value }));

  return (
    <StyledSignupFieldsContainer>
      <StyledInputContainer
        placeholder='ex)인디벗'
        title='닉네임'
        extraAction={dispatchUsername}
        validationMessage='한글/영문 2-10자로 작성해주세요.'
        validationFunction={usernameValidityTest}
      ></StyledInputContainer>
      <StyledInputContainer
        placeholder='ex)inddy@gmail.com'
        title='아이디'
        extraAction={dispatchEmail}
        validationMessage='유효한 이메일 형식이 아닙니다.'
        validationFunction={emailValidityTest}
      ></StyledInputContainer>
      <StyledInputContainer
        placeholder='8자리 이상'
        title='비밀번호'
        extraAction={dispatchPassword}
        validationMessage='비밀번호는 8-16자 영문,숫자,특수문자의 조합이어야 합니다.'
        validationFunction={passwordValidityTest}
        type='password'
      ></StyledInputContainer>
    </StyledSignupFieldsContainer>
  );
};

export default SignupFieldsContainer;

const StyledSignupFieldsContainer = styled.div`
  margin-bottom: 24px;
`;

const StyledInputContainer = styled(InputContainer)`
  margin-bottom: 24px;
`;

이 부분은 오히려 코드가 좀 길어졌지만, InputContainer에 유효성검사 함수, store에 상태 저장하는 함수를 넘겨주어야 하기 때문인데 이 코드들을 useInput과 useValidation에서 가져온 것이라 코드가 길어진 것은 아니다. 

이 함수들을 util 같은 폴더로 빼면 더 완벽하지만 그 부분은 부차적인 것이라 머리를 식힐 때 추가적으로 리펙토링을 완성해보겠다.

 

그리고, InputContainer를 마지막으로 확인해보면,

 

수정 전

import styled from 'styled-components';
import { StyledInput } from '../elements/Input';
import { Label } from '../elements/Label';
import { InputContainerType } from '../../types/componentsTypes';
import useInput from '../../hooks/useInput';

const InputContainer = ({
  placeholder = 'ex)',
  title = '입력창',
  validationMessage,
  validationType = 'none',
  validationFunction,
  type
}: InputContainerType) => {
  const useInputResult = useInput('', validationType, validationFunction);
  // 현재 유효성검사 useInput에 포함되어서 처리 중인데,
  // 관심사 분리에 따라 useValidation으로 분리해서 하는 것도 좋아보임.

  return (
    <InputContainerWrapper>
      <StyledLabelContainer>
		const InputContainer = ({
          placeholder={placeholder}
          value={useInputResult ? useInputResult.value : ''}
          type={type ? type : ''}
          validationFunction={validationFunction}
          onChange={
            useInputResult
              ? useInputResult.onChange
			  : () => {
                  console.log('error');
                }
          }
        />
        {!useInputResult.validity ? (
          useInputResult?.value === '' ? (
            <ValidMessageInvisible className='invisible'>
              Invisible
            </ValidMessageInvisible>
          ) : (
            <ValidMessage className='msg'>{validationMessage}</ValidMessage>
          )
        ) : (
          <ValidMessageInvisible>Invisible</ValidMessageInvisible>
        )}
      </Field>
    </InputContainerWrapper>
  );

 

수정 후

...
const InputContainer = ({
  placeholder = 'ex)',
  title = '입력창',
  extraAction,
  validationMessage,
  validationType = 'none',
  validationFunction,
  type
}: InputContainerType) => {
  const useInputResult = useInput('', validationType, validationFunction);
  // 현재 유효성검사 useInput에 포함되어서 처리 중인데,
  // 관심사 분리에 따라 useValidation으로 분리해서 하는 것도 좋아보임.
  const useInputResult = useInput('', extraAction, validationFunction);

  return (
    <InputContainerWrapper>
      <StyledLabelContainer>
        <StyledLabel>{title}</StyledLabel>
      </StyledLabelContainer>
      <Field>
        <InputEl
          placeholder={placeholder}
          value={useInputResult ? useInputResult.value : ''}
          type={type ? type : ''}
          validationFunction={validationFunction}
          onChange={
            useInputResult
              ? useInputResult.onChange
              
...

이렇게 extraAction으로 validationtype을 없앤 역할을 일부 대신하도록 하고, validationFuction도 필요한 경우에 선택적으로 넣을 수 있도록 하였다.  

 

 

 

#5 404페이지

404페이지를 왜 이렇게 오랜시간을 들여서 만들었는지.. CSS가 완전 익숙하지 않아서 처음에 404페이지 큰 이미지를 fixed로 했다가 가장 바깥에 있는 div 틀을 건드릴 수가 없어서... 가운데정렬 속성을 주어야하는데 그렇게하면 모든 페이지에서 네비게이션 바랑 푸터 등 틀이 완전 틀어지기 때문에 차마 그렇게 할 수가 없었다.

 

현재 이렇게 구현이 되어있고 반응형으로는 크기가 전반적으로 80%로 작아지도록 하였는데... Button element를 공용으로 맨처음에 만든 것이 괜히 함수형컴포넌트를 만들어놓아서 그런지 활용법이 익숙하지 않아서 className을 어떻게 추가해야할지도 당장 모르겠고 스타일 적용도 덮어씌워서 하고싶은데 styledComponents로 다시 스타일을 지정해도 적용이 전혀 안되어서 Back to Home 부분의 글씨 크기가 모바일버젼에서 현재 조정이 안되는 문제가 있다...ㅜ 뤼튼이가 해결해줄 수 있으려나..? 사실 이 부분은 컴포넌트 구조를 보내더라도 쉽게 해결해주지 못할 것 같아서 일단 보류해두고 검색과 메세지 기능을 완성하고 주말에 시간이 되면 더 만져보도록 해야겠다.

 

가능하면 애니메이션이 들어간 404페이지로 만들고 싶다..ㅎㅎ CSS 실력도 좀 늘릴 겸!!

 

#6 기타 detail한 고민

1) 자동배포이슈

2) 게임팔로우 클릭 시 다른 유저에게 팔로우 수가 업데이트 되는 로직

post 요청 > back에서 response 

3) 개별게임채널에 추가기능

게임 이미지에 hover 했을 때, 게임설명 overlay되는 기능,

게임 이미지를 클릭했을 때 다운로드링크로 이동하는 기능