프론트엔드 면접준비1

이동현·2025년 3월 27일
0

1. JavaScript

 🔎 JS이벤트루프 (비동기처리)

    📕 개요

      1. : 파일다운, 네트워크요청, 타이머, 애니메이션등 오래걸리고 반복적인 작업들은 브라우저 내부 멀티스레드인 WebAPIs 에서 비동기 + 논블로킹으로 처리(메인스레드가 다른곳에 요청하여 대신실행하고 작업이 완료되면 이벤트/콜백함수를 받아 결과를 실행하는 방식)

      2. : 이벤트루프는 브라우저의 Call Stack/Heap, Callback Queue, WebAPIs등을 순서대로 처리하는 브라우저 동작 타이밍제어 관리자

        1) CallStack: js엔진이 코드 실행을 위해 사용하는 메모리구조
        2) Heap: 동적으로 생성된 js객체가 저장되는 공간
        3) WebAPIs: 에)Ajax호출, 타이머함수, Dom조작
        4) Callback Queue: 비동기적 작업이 완료되면 실행되는 함수들이 대기하는공간

  🔎 호이스팅(Hoisting)

    📕 개요

      1. 변수의 정의가 그범위에 따라 선언과 할당으로 분리되는것, 전역에서 선언된 변수는 전역 컨텍스트 최상위로 끌어올려지며 함수내에서 선언된 변수는 함수 최상위로 끌어올려짐.

      2. var 키워드로 선언한 변수는 런타임 이전에 JS엔진에 의해 암묵적으로 선언 초기화가 한번에 진행, let과const는 선언단계가 이뤄지고 런타임 때 초기화와 할당이 이뤄짐

  🔎 Scope(Lexical Scope)

    📕 개요

      1. 모든식별자는 자신이선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효범위가 결정되는것

      2. 스코프가 계층적으로 연결된것을 스코프체인 이라고 한다. 하위스코프 기준으로 위로는 검색하지만 아래로는 검색하지 않음.

      3. var 키워드로 선언된 변수는 오로지 함수 코드블록만을 지역스코프로 인정하기 때문에 이러한 특성을 [함수레벨 스코프]라고 한다.

  🔎 Closure, Currying

    📕 Closure

      1. 자신이 생성될때 상위 스코프 렉시컬환경을 기억하고 있어 외부함수의 실행이 끝나서 실행 컨텍스트에서 소멸된 이후에도 외부함수의 렉시컬 환경을 내부함수가 참조하고 있기 때문에 내부함수가 외부함수 변수에 접근이 가능한것

      2. 장점: 전역변수를 줄일 수 있다, 코드 재사용률을 높일 수 있다.

  🔎 this

  🔎 class, instance

  🔎 비동기처리(Promise, asnyc/await)

    📕 Promise

      1. 콜백 헬 단점을 보완하여 비동기 시점을 명확하게 표현 할 수 있다. resolve와 rejet함수를 인자로 전달받아 각각 성공/실패 시 호출 한다.

      2. setTimeout()보다 Promise()가 먼저 실행되는 이유 => Promise.resolve('Promise!!').then(res => console.log('promise out'))는 Microtask Queue에(*Tassk Queue보다 우선순위 높음), setTimeout( )는 Macrotask Queue에 적재되어서 우선순위가 높은 Microtask Queue가 먼저 실행

    📕 Async/Await

      1. : 예시1

const one = () => Promise.resolve('ONE');

async function myFunc(){
	console.log('In Function');
    const res = await one();
    console.log(res)
}

console.log('Before Func');
await myFunc();
console.log('After Func');

[변환]
const one = () => Promise.resolve('ONE');

async function myFunc(){
	console.log('In Function');
    
    return one().then(res => {
    	console.log(res)
    })
    
}

console.log('Before Func');
myFunc().then(() => {
	console.log('After Func');
})

  🔎 기타

    📕 const 키워드에서 array데이터는 삽입,수정,삭제가 되는 이유

      1. const 타입을 가진 변수는 재할당이 금지되어있기 때문에 메모리 주소를 변경할수 없지만 const 객체는 값을 가리키는 메모리 주소를 참조 하고 있으며 그참조된 주소 값은 변경이 가능한 값이므로

2. Frontend기본

  🔎 HttpRequest

    📕 GET

      :GET은 주로 서버에서 정보를 조회하기 위해 사용하며, 요청하는 데이터는 URL의 쿼리 문자열에 포함되어 전송됩니다. 이로 인해 URL에 데이터가 노출되는 것이 필연적이며, 크기 제한이 있습니다.

    📕 POST

      :POST는 데이터를 서버에 제출할 때 사용되며, 데이터는 요청 본문에 포함됩니다. 따라서 데이터의 크기 제한 없으며, 민감하고 보안이 필요한 정보 전송에 사용하는 것이 권장됩니다.

  🔎 RestAPI

  🔎 Event(Bubbling, Capturing, Delegation)

  🔎 RequestAnimationFrame

  🔎 Module Bundler(Webpack, Vite, ESBuild)

  🔎 Babel

  🔎 컴파일, 트랜스파일

  🔎 웹접근성, 시맨틱웹

  🔎 CSR, SSR

    📕 개요

      1. CSR : 렌더링이 클라이언트에서 발생하며 페이지 이동속도가 빠르지만 SEO에 불리하고 첫로딩단계에서 빈페이지가 표시되어 UX에 부정적인 영향을 줄수 있다.

      2. SSR : 서버에서 렌더링 준비를 마친 페이지를 생성해 클라이언트에 전송하는 방식, 브라우저에서 초기 로딩을 마친 페이지가 사용자에게 바로 보여지고 SEO에 ㅇ리하고 첫로딩이 빠르지만 서버 부하가 커지거나 페이지 이동속도가 보다 느릴 수 있다. 이때 ISR기술을 사용해 캐싱된 페이지를 보여줘 서버 부하를 낮춘다.

        * ISR : 웹사이트의 일부페이지를 정적으로 생성하면서 해당페이지 콘텐츠를 동적으로 업데이트 할 수 있는 기능(Next 9.5부터 도입)
          1) 페이지 설정 및 데이터준비
          2) getStaticProps사용 : 이함수는 정적생성시점에 한번 호출되어 초기 데이터를 가져오고 반환한 데이터를 페이지 컴포넌트로 전달하며 revalidate옵션은 페이지의 얼마나 자주 재생성할지를 설정.
          3) getStaticPath사용 : 동적경로를 가진 페이지에 ISR을 적용하려면 이함수 사용. 이함수를 사용하면 어떤 경로가 사전에 생성될지 ㅈ정 할 수 있다.

  🔎 웹, 앱브릿지

  🔎 CORS와 해결경험

    📕 개요 : 웹어플리케이션에서 다른 도메인의 리소스에 접근할때 발생하는 보안 이슈 해결을 위한 표준방법. CORS는 브라우저 단에서 작동하며 웹서버가 특정 HTTP헤더를 반환함으로 웹브라우저가 자원에대한 권한을 부여

    📕 해결방법

      1. 서버에서 해결: 서버에서 Access-Control-Allow-Origin 헤더에 유효값을 포함해 응답을 브라우저로 보내서 해결.

      2. 프론트에서해결: 다른사람이만든 프록시 서버이용하기-> 요청해야하는 URL앞에 프록시서버URL을 붙여서 요청

      3. 로컬한정: 배포전 로컬환경에서 라이브러리 사용으로 해결

const {createProxyMiddleware} = require("http-proxy-middleware");

module.exports = function(app){
	app.use("/api", createProxyMiddleware({ target: '', changeOrigin: true}))
}

  🔎 쿠키,세션

    📕 차이점

      1. 쿠키-클라이언트, 세션-서버에 저장되고 보안은 세션 > 쿠키, 속도는 쿠키 > 세션이다.

      2. 쿠키는 브라우저를 종료해도 유지O 만료기간이 종료되어도 브라우저가 인식만 못할뿐 파일이 삭제는 되지않음 반면 세션은 브라우저 메모리에 저장되므로 브라우저가 종료되면 세션ID가 삭제된다.

  🔎 JWT

    📕 개요: 서버측에서 토큰을 발급해 클라이언트에 전달하면 클라이언트 측에서 토큰을 가지고 인가를 수행

      1. 구성 : HEADER, PAYLOAD, SIGNATURE가 .으로 구분되어있다.

    📕 특징

      1. 무상태성을 가지고 있기때문에 세션을 유지할필요 없다

      2. JSOn형태로 구성되어있어 파싱하기 쉽고 다양한 언어에서 지원된다

      3. URL,POST변수,HTTP헤더 어디서든 전송이 가능하다.

3. React

  🔎 VirtualDOM이 무엇인지?

    📕 VirtualDOM의 개념

    📕 VirtualDOM의 업데이트방식

      1. 가상dom트리생성(원본dom과 동일한)

      2. 데이터 변경시 애플리케이션 가상dom에 변경사항 적용

      3. 가상dom에 내용과 현재 내용을 비교

      4. 바뀐부분을 실제dom에 적용

    📕 VirtualDOM의 성능최적화 이유

      1. 리액트에서의 버츄얼 돔은 DOM을 객체화 시킴으로써, 변경감지에 대한 최적화를 시킬수 있고 그로인해 불필요한 리렌더링이 감소시키기 위함이다.

      2. 결국 변경된 부분만 감지해서 효율적으로 렌더링 이벤트를 발생시킨다는것 때문이다.

      3. useState, useEffect같은 Hook을 사용하여 컴포넌트의 상태관리와 부수효과 처리할때 의존성 배열을 적절히 관리해 불필요한 업데이트를 방지 할 수 있다.(Hook실행조건 제어)

  🔎 데이터 바인딩 방식

      1. 기본적으로 React는 단방향 데이터 바인딩방식(부모 -> 자식)

        1) 예측가능한 데이터 흐름을 만들기위해 : 단방향 데이터 흐름은 데이터의 흐름이 항상 한 방향으로만 이동하도록 한다. 부모 컴포넌트에서 자식 컴포넌트로 데이터가 전달되는 방향이다. 이로 인해 데이터의 흐름이 예측 가능하며, 컴포넌트 간의 의존성이 명확하게 드러난다.
        2) 디버깅 용이성 : 데이터 흐름이 한 방향으로만 이동하면 디버깅이 간단해진다. 특정 컴포넌트에서 데이터의 변경이 발생하면, 그 변경이 일어난 부모 컴포넌트에서부터 디버깅을 시작할 수 있다.
        3) 컴포넌트 독립성 : 자식컴포넌트는 부모로부터 전달받는 props를 통해 데이터에 접근하고 수정할수 없다. 이는 컴포넌트간 결합도는 낮추고 재사용성을 향상
        4) 양방향 바인딩 예시:
import React, { useState, useCallback } from 'react';

function InputComponent() {
  // 상태 초기화
  const [inputValue, setInputValue] = useState('');

  // 입력값을 업데이트하는 함수 (useCallback 사용)
  const handleInputChange = useCallback((event) => {
    setInputValue(event.target.value);
  }, []);  // 의존성 배열이 비어 있으므로 함수는 한 번만 생성됨

  return (
    <div>
      <input 
        type="text" 
        value={inputValue}  // input의 값은 상태에 바인딩
        onChange={handleInputChange}  // input의 변화가 있을 때 상태를 업데이트
      />
      <p>입력값: {inputValue}</p>
    </div>
  );
}

export default InputComponent;

  🔎 Flux구조

    📕 개요

      1. 특징: 단방향 데이터흐름을 유지하기위해 사용자입력기반 Action만들고 Dispatcher에 전달하여 Store 데이터를 변경한뒤View에 반영

    📕 예시

      1.

[store.js]
const initialState = {
  number: 0
};

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return {
        number: state.number + 1
      }
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  
  const onAdd = () => {
    dispatch({type: 'ADD'})
  }
  
  return (
    <div>
      <h1>{state.number}</h1>
      <button onClick={onAdd}>Add</button>
    </div>
  )
}

  🔎 Memorization(useMemo, useCallback, React.memo)

    📕 useMemo(콜백함수, 의존성배열)

      1. 특징:

        1) 계산비용이 높은 연산결과를 메모제이션하여 불필요한 연산을 최소화하는 Hook 즉, useMemo사용해서 결과값을 캐싱하면 렌더링중 값이 한번만 계산이 되어 이후 렌더링할경우 의존성배열의 값이 달라지지 않으면 이전 연산값 그대로 사용 (배열이나 객체를 가공, 복잡한 계산 수행, API호출해서 데이터 가져올때 사용)

          ex)

[개선전]
const PrimeNumbers = ({ max}) => {
	const primes = calculatePrimeNumbers(max);
    return(
    	<div>
        	<p>{primes.join(', ')} </p>
        </div>
    )
};

const App = () => {
	const [max, setMax] = useState(10);
    return(
    	<div>
        	<PrimeNumbers max={max} />
        </div>
    )
}

[개선후]
const PrimeNumbers = ({ max}) => {
	const primes = useMemo(() => calculatePrimeNumbers(max));
    return(
    	<div>
        	<p>{primes.join(', ')} </p>
        </div>
    )
};

const App = () => {
	const [max, setMax] = useState(10);
    return(
    	<div>
        	<PrimeNumbers max={max} />
        </div>
    )
}
        개선전에는 calculatePrimeNumbers 함수가 입력값이 변경될때마다 재실행해서 소수계산, 개선후에는 입력값이 변경되지 않으면 이전계산된 소수목록을 재사용하여 성능 개선

    📕 useCallback()

      1. 특징:

        1) 이전에 한번 생성된 함수를 기억해서 불필요하게 함수가 재생성되는것을 방지

          ex)

const TestComponent = () => {
	const [number, setNumber] = useState(0);
    
    const increaseNumber = useCallback(() => {
    	number++;
    }, [number]);
    
    return (
    	<div>
        	<button onClick = {increaseNumber}> + </button>
        </div>
    )
}
        위에서 의존성 배열number의 상태값이 변경되지 않으면 이전생성된 후 메모제이션 되었던 함수가 재사용된다.(컴포넌트에 전달되는 Callback함수, 자주호출되는 Callback함수, 렌더링 성능이 최적화된 자식 컴포넌트(React.memo로 감싼컴포넌트)에 함수를 Props로 넘겨주는경우, 큰비용을 필요로하는 함수를 생성할때)

    📕 React.memo

      1. 특징:

        1) props의 변경없이 부모 컴포넌트가 렌더링될때 자식 컴포넌트도 함께 렌더링되는것을 방지 (props가 변경되었는지 확인하고 변경되지 않았다면 이전 렌더링결과를 재사용.)
[개선전]
const ChildComponent = ({ count }) => {
	return <div>Count: {count}</div>
};

const ParentComponent = () => {
	const [count, setCount] = useState(0);
    const [inputValue, setInputValue] = useState('')
    
    return(
    	<div>
        	<button onClick={() => setCount(count +1)}>증가</button>
            <input 
            	type="text"
                value={inputValue}
                onChange= {(e) => setInputValue(e.target.value)}
            />
            <ChildComponent count={count} />
        </div>
    )
};

[개선후]
const ChildComponent = React.memo(({ count }) => {
	return <div>Count: {count}</div>
});

const ParentComponent = () => {
	const [count, setCount] = useState(0);
    const [inputValue, setInputValue] = useState('')
    
    return(
    	<div>
        	<button onClick={() => setCount(count +1)}>증가</button>
            <input 
            	type="text"
                value={inputValue}
                onChange= {(e) => setInputValue(e.target.value)}
            />
            <ChildComponent count={count} />
        </div>
    )
};

  🔎 Reconciliation, Fiber

  🔎 ReactEvent처리방식(Delegation)

  🔎 Component Key Props

  🔎 ShallowCopy, DeepCopy

  🔎 선언형 프로그래밍

  🔎 React18(Suspense, useDeferredValue, useTransition)

  🔎 기본적인 Hooks

    📕 useEffect()

      1. 외부시스템과 컴포넌트를 동기화 할 수 있게 해주는 리엑트 훅

        1) API통신
        2) ref를 이용한 dom제어
        3) window 전역 객체 이벤트 리스너 할당/해제
        4) 예시 :

[훅]
import { useState, useEffect } from 'react';

// 데이터 로딩 훅
function useDataFetching(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  const fetchData = async () => {
    try {
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
    } catch (error) {
      setError(error);
    } finally {
      setLoading(false);
    }
  };

  fetchData();
}, [url]); // url이 바뀔 때마다 데이터를 다시 fetch

return { data, loading, error };
}

// 윈도우 크기 감지 훅
function useWindowSize() {
const [size, setSize] = useState({
  width: window.innerWidth,
  height: window.innerHeight,
});

useEffect(() => {
  const handleResize = () => {
    setSize({
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };

  window.addEventListener('resize', handleResize);

  // cleanup 함수: 이벤트 리스너를 제거
  return () => window.removeEventListener('resize', handleResize);
}, []); // 처음 렌더링 시 한 번만 실행

return size;
}

function useCustomHook(url) {
const { data, loading, error } = useDataFetching(url);
const size = useWindowSize();

return { data, loading, error, size };
}

export default useCustomHook;


[컴포넌트]
import React from 'react';
import useCustomHook from './useCustomHook';

function App() {
const { data, loading, error, size } = useCustomHook('https://jsonplaceholder.typicode.com/posts');

if (loading) {
  return <div>Loading...</div>;
}

if (error) {
  return <div>Error: {error.message}</div>;
}

return (
  <div>
    <h1>Data:</h1>
    <ul>
      {data.slice(0, 5).map((item) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>

    <h2>Window Size:</h2>
    <p>Width: {size.width}px</p>
    <p>Height: {size.height}px</p>
  </div>
);
}

export default App;
        * 커스텀 훅에서 여러개 useEffect사용하는 이유?
          1. 구분된 로직 : 서로다른 종류의 사이드 이펙트를 처리해서 하나는 데이터 요청을 처리, 다른하나는 DOM이벤트를 처리
          2. 유지보수성 : 로직을 컴포넌트에서 분리하고 각기능을 커스텀 훅으로 만들면 재사용 가능, 유지보수성 용이
          3. 성능최적화 : 다른용도로 분리하면 불필요한 리렌더링을 방지.

      2. 주의할점:

        1) 의존성배열 잘넘겨주기 (의존성 배열인자에 아무것도 넘겨주지 않으면 의존성이 undefined상태가 되어 렌더링 전후 비교를 할수 없어 무한루프에 빠짐)
        2) useEffect 의존성 배열 상태를 useEffect에서 변경되지 않게 하기
        3) useEffect에서 사용되는 함수는 useEffect안에 놓기
        4) 여러 useEffect에서 함수를 공유할 때는 useCallback사용하기
          ex)
import React, { useState, useEffect, useCallback } from 'react';

const MyComponent = () => {
  const [count, setCount] = useState(0);
  const [isVisible, setIsVisible] = useState(true);

  // useCallback을 사용하여 함수 메모이제이션
  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // 의존성이 빈 배열이므로 컴포넌트가 마운트될 때 한 번만 생성됩니다.

  const toggleVisibility = useCallback(() => {
    setIsVisible((prev) => !prev);
  }, []); // 의존성이 빈 배열이므로 컴포넌트가 마운트될 때 한 번만 생성됩니다.

  useEffect(() => {
    // handleClick 함수를 사용할 때마다
    console.log('handleClick has been called');
  }, [handleClick]); // handleClick이 변경될 때마다 실행됩니다.

  useEffect(() => {
    // toggleVisibility 함수를 사용할 때마다
    console.log('toggleVisibility has been called');
  }, [toggleVisibility]); // toggleVisibility가 변경될 때마다 실행됩니다.

  return (
    <div>
      <button onClick={handleClick}>Increment</button>
      <button onClick={toggleVisibility}>Toggle Visibility</button>
      <div>{isVisible ? 'Visible' : 'Hidden'}</div>
      <div>Count: {count}</div>
    </div>
  );
};

export default MyComponent;

    📕 useCallback()

      1. 특징:

        1) 메모제이션된 함수를 반환(특정 의존성이 변경되지 않는 한 동일한 함수 인스턴스를 재사용, 불필요한 렌더링 방지.)

    📕 useReducer()

      1. 특징: useState()와 많이비교, 복잡한 상태관리에서 사용. 이전상태와 action을 합쳐 새로운state를 만드는 조작. 콜백대신dispatch를 전달

import React, { useReducer } from 'react';

// 초기 상태
const initialState = {
  isLoggedIn: false,
  user: null,
};

// reducer 함수 정의
function reducer(state, action) {
  switch (action.type) {
    case 'LOGIN':
      return { ...state, isLoggedIn: true, user: action.payload };
    case 'LOGOUT':
      return { ...state, isLoggedIn: false, user: null };
    default:
      return state;
  }
}

function UserProfile() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const handleLogin = () => {
    const user = { name: 'John Doe', age: 30 };  // 예시 사용자 정보
    dispatch({ type: 'LOGIN', payload: user });
  };

  const handleLogout = () => {
    dispatch({ type: 'LOGOUT' });
  };

  return (
    <div>
      {state.isLoggedIn ? (
        <div>
          <h1>Welcome, {state.user.name}</h1>
          <p>Age: {state.user.age}</p>
          <button onClick={handleLogout}>Logout</button>
        </div>
      ) : (
        <div>
          <h1>Please log in</h1>
          <button onClick={handleLogin}>Login</button>
        </div>
      )}
    </div>
  );
}

export default UserProfile;

    📕 useRef()

      1. React19에서 바뀐점

        1) forwardRef() 가 없어짐

이전버전

import React, { forwardRef } from 'react';
 const MyButton = forwardRef((props, ref) => (
  <button ref={ref} {...props}>
    {props.children}
  </button>
));
 
// 사용 예시
const App = () => {
  const buttonRef = React.useRef();
  return <MyButton ref={buttonRef}>Click Me</MyButton>;
};

React19에서 변화

import React, { forwardRef } from 'react';
type MyComponentProps = {
  ref?: React.Ref<HTMLDivElement>;
  children: React.ReactNode;
};
 
const MyButton = ({ ref, ...props }:MyComponentProps) => {
  return (
    <button ref={ref} {...props}>
      {props.children}
    </button>
  );
};
 
// 사용 예시
const App = () => {
  const buttonRef = React.useRef();
  return <MyButton ref={buttonRef}>Click Me</MyButton>;
};

  🔎 상태관리(Client-side, Server-side)

    📕 contextAPI vs Redux

      1. 차이점요약:

특징Context APIRedux
설정간단하게 설정 가능설정이 복잡하고 보일러플레이트가 많음
상태 관리 범위주로 작은 애플리케이션에서 전역 상태 관리대규모 애플리케이션에서 복잡한 상태 관리
성능상태가 커지면 성능 저하 가능성능 최적화가 잘 되어 있고 대규모 애플리케이션에서 안정적
미들웨어미들웨어 없음미들웨어(예: Redux Thunk, Saga 등) 지원
사용 예간단한 상태 공유 (예: 테마, 로그인 상태 등)복잡한 상태 관리가 필요한 대규모 애플리케이션

    📕 Redux

      ex)
[action.js]
// actions.js
export const fetchDataRequest = () => ({
  type: 'FETCH_DATA_REQUEST',
});

export const fetchDataSuccess = (data) => ({
  type: 'FETCH_DATA_SUCCESS',
  payload: data,
});

export const fetchDataFailure = (error) => ({
  type: 'FETCH_DATA_FAILURE',
  payload: error,
});

// 비동기 API 호출을 처리하는 액션 (redux-thunk 사용)
export const fetchData = () => {
  return (dispatch) => {
    dispatch(fetchDataRequest()); // 요청 시작 액션
    fetch('https://jsonplaceholder.typicode.com/posts') // 예시 API 호출
      .then((response) => response.json())
      .then((data) => dispatch(fetchDataSuccess(data))) // 성공 시 데이터 저장
      .catch((error) => dispatch(fetchDataFailure(error))); // 실패 시 에러 처리
  };
};

[reducer.js]
// reducer.js
const initialState = {
  loading: false,
  data: [],
  error: '',
};

const dataReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'FETCH_DATA_REQUEST':
      return { ...state, loading: true };
    case 'FETCH_DATA_SUCCESS':
      return { ...state, loading: false, data: action.payload };
    case 'FETCH_DATA_FAILURE':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
};

export default dataReducer;


[store.js]
// store.js
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import dataReducer from './reducer';
import thunk from 'redux-thunk';

// 미들웨어 적용
const store = createStore(dataReducer, applyMiddleware(thunk));

export default store;

[App.js]
// App.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchData } from './actions'; // 비동기 액션 임포트

const App = () => {
  const dispatch = useDispatch();
  const { loading, data, error } = useSelector((state) => state);

  // useEffect를 사용하여 초기 데이터 요청
  useEffect(() => {
    dispatch(fetchData()); // 컴포넌트가 마운트될 때 데이터 로드
  }, [dispatch]);

  return (
    <div>
      <h1>Data Fetch Example</h1>
      <button onClick={() => dispatch(fetchData())}>Fetch Data</button>

      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}

      <ul>
        {data && data.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default App;

    📕 ReactQuery

      1. 선택이유 : Redux를 사용할때는 하나의 api를 관리한다고 했을때 이에 상응하는 store, action, isLoading, isSuccess, isError를 하나씩 수동으로 컨트롤 해야하는 문제점이 있어 서버상태를 불러오고 캐싱하고 지속적인 동기화와 업데이트를 쉽게 하기 위해서

  🔎 컴포넌트 설계

    📕 AtomicDesign : 컴포넌트를 작은단위 원자,분자,유기체,템플릿,페이지 단위로 나눔

      1. 특징:

        1) 재사용성
        2) 유지보수 용이
        3) 일관성
        4) 협업모듈성

  🔎 Props Drilling

    📕 개요 : 상위컴포넌트에서 하위컴포넌트로 데이터를 전달할때 중간 컴포넌트들을 거쳐야 하는과정

    📕 문제점

      1. 유지보수 어려움

      2. 코드의 가독성 저하

      3. 재사용성 감소

    📕 해결방법

      1. ContextAPI사용

import React, { createContext, useContext } from 'react';

// Context 생성
const UserContext = createContext();

// 최상위 컴포넌트
function App() {
  const user = { name: 'John Doe', age: 30 };

  return (
    <UserContext.Provider value={user}>
      <h1>App Component</h1>
      <Parent />
    </UserContext.Provider>
  );
}

// 부모 컴포넌트
function Parent() {
  return (
    <div>
      <h2>Parent Component</h2>
      <Child />
    </div>
  );
}

// 자식 컴포넌트
function Child() {
  return (
    <div>
      <h3>Child Component</h3>
      <Grandchild />
    </div>
  );
}

// 손자 컴포넌트
function Grandchild() {
  // Context에서 user 데이터 가져오기
  const user = useContext(UserContext);

  return (
    <div>
      <h4>Grandchild Component</h4>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
    </div>
  );
}

export default App;

      2. Children props사용

import React from 'react';

// 최상위 컴포넌트
function App() {
  const user = { name: 'John Doe', age: 30 };

  return (
    <div>
      <h1>App Component</h1>
      <Parent user={user}>
        {/* 중간 컴포넌트에 user 데이터를 children으로 전달 */}
        <Child />
      </Parent>
    </div>
  );
}

// 부모 컴포넌트
function Parent({ user, children }) {
  return (
    <div>
      <h2>Parent Component</h2>
      {/* 자식 컴포넌트를 렌더링하면서 children으로 전달된 컴포넌트에게 user 데이터를 props로 전달 */}
      {React.cloneElement(children, { user })}
    </div>
  );
}

// 자식 컴포넌트
function Child({ user }) {
  return (
    <div>
      <h3>Child Component</h3>
      <Grandchild user={user} />
    </div>
  );
}

// 손자 컴포넌트
function Grandchild({ user }) {
  return (
    <div>
      <h4>Grandchild Component</h4>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
    </div>
  );
}

export default App;

4. Vue

  🔎 Single-File-Components

    📕 defineModel(): Vue3.3이상에서 도입된 CompositionAPI에서 양방향바인딩(v-model)을 더간략히 쓸수 있다.

      기존

	const props = defineProps(['modelValue'])
	const emit = defineEmits(['update:modelValue'])

      변경

  [자식Component]
  <script setup>
  const model = defineModel(); // 기본적으로 v-model을 처리
  </script>

  <template>
    <input v-model="model" class="border p-2" />
  </template>

  [부모Component]
  <script setup>
  import MyInput from './MyInput.vue'
  import { ref } from 'vue'

  const name = ref('')
  </script>

  <template>
    <MyInput v-model="name" />
    <p>입력한 값: {{ name }}</p>
  </template>

    📕 defineExpose(): 자식컴포넌트 내부 메소드나 변수를 부모가 쓸수 있도록 노출하는 기능.

      1. 언제사용?

        : 부모가 자식컴포넌트의 함수,변수에 직접접근하고싶을때 (예:focusInput())

      [자식]

    <script setup>
    import { ref } from 'vue'

    const inputRef = ref(null)

    const focusInput = () => {
      inputRef.value?.focus()
    }

    // 부모에서 사용할 수 있도록 expose!
    defineExpose({
      focusInput
    })
    </script>

    <template>
      <input ref="inputRef" placeholder="Type here..." class="border p-2" />
    </template>

      [부모]

    const inputComponent = ref(null)

    onMounted(() => {
      // 자식의 focusInput() 메서드 호출!
      inputComponent.value.focusInput()
    })
    </script>

    <template>
      <MyInput ref="inputComponent" />
    </template>

      2. defineModel()과 같이사용되는경우: 입력창 초기화, 포커스 제어, 자식 내부 상태 리셋

      [자식]

    <script setup>

    // 부모로부터 v-model로 값을 전달받음
    const text = defineModel<string>()

    // 내부 input 참조
    const inputRef = ref<HTMLInputElement | null>(null)

    // 부모에서 호출할 수 있게 노출할 메서드
    function clear() {
      text.value = ''
      inputRef.value?.focus()
    }

    // expose
    defineExpose({
      clear
    })
    </script>

    <template>
      <input
        ref="inputRef"
        v-model="text"
        placeholder="Type something..."
        class="border px-2 py-1 rounded"
      />
    </template>

      [부모]

    <script setup>
    const message = ref('')
    const textInputRef = ref(null)

    // 버튼 클릭 시 자식의 clear() 호출
    const clearText = () => {
      textInputRef.value?.clear()
    }
    </script>

    <template>
      <TextInput v-model="message" ref="textInputRef" />

      <p class="mt-2">입력한 메시지: {{ message }}</p>

      <button @click="clearText" class="mt-2 px-4 py-1 bg-blue-500 text-white rounded">
        Clear from Parent
      </button>
    </template>

  🔎 Composition API

    📕 vs Option API

      : Composition API는 setup() 함수 내부에 로직을 함수형으로 분리해서 작성하며, 재사용성과 테스트 용이성이 높습니다.반면 Options API는 data, methods, computed 등으로 옵션을 구분하는 방식이라 구조는 명확하지만, 로직이 분산되기 쉽습니다.저는 복잡한 비즈니스 로직이나 컴포지션 훅 활용이 필요한 컴포넌트에선 Composition API를 주로 사용하고, 간단한 UI 로직에선 Options API도 사용합니다.

  🔎 Vue3반응형 시스템

    📕 ref

    📕 reactive : 객체에 Proxy를 씌어서 객체 내부까지 반응형으로 만드는 속성입니다.

    📕 computed : 의존 값이 바뀔 때만 자동으로 재계산되는 캐싱된 속성입니다.

=> 저는 form 상태 관리엔 reactive를, 단일 값이나 primitive엔 ref, 파생된 데이터 계산엔 computed를 사용합니다.

    📕 Provide/Inject : 자식 컴포넌트 간의 깊은 상태 전달을 위해 사용됩니다. 예를 들어 테마, 다국어 설정, 폼 컨텍스트 등에서 중간 컴포넌트를 거치지 않고 하위 컴포넌트에 데이터를 전달할 때 사용했습니다.다만, 양방향이 아닌 단방향이며, 컴포넌트 간 결합도가 높아질 수 있어 상황에 따라 Pinia 같은 상태 관리 도구와 병행합니다.

  🔎 빌트인 컴포넌트

    📕 keep-alive : 여러 컴포넌트 간에 동적으로 전환될 때, 컴포넌트 인스턴스를 조건부로 캐시할 수 있는 빌트인 컴포넌트입니다.

      📖활용: 탭이나 페이지 이동 시, 각 탭의 상태를 유지하고 싶을 때, 데이터를 불러오는 데 시간이 많이 소요되는 컴포넌트의 상태를 유지하고 싶을 때 사용된다. 아래 예시에서 HelloWorld, AbcWorld 서브 컴포넌트가 현재 컴포넌트에서 교체되어도 데이터는 유지된다.

<template>
	<KeepAlive>
    	<component :is="currentComponent" />
    </KeepAlive>
    <button @click="currentComponent = HelloWorld">Go Hello </button>
    <button @click="currentComponent = AbcWorld">Go Abc </button>
</template>
<script setup>
	import HelloWorld from '../HelloWorld.vue';
</script>

    📕 transition : 요소가 DOM에 삽입,삭제 되었을때 자연스럽게 변화할수 있게 해주는 컴포넌트.

  🔎 기타

    📕 lazy-loading

      💡Lazy-Loading Routes
      : 아래 예시에서 '기존'프로젝트에서는 최상단에 필요한 view파일들을 import하여 선언하지만 아래에서는 진입점이 맞는 컴포넌트들만 선언.
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      // Component here is NOT lazy loaded
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
      // Component here is lazy loaded
      component: () => import('../views/AboutView.vue')
    },
    {
      path: '/contact',
      name: 'contact',
      // Component here is lazy loaded
      component: () => import('../views/ContactView.vue')
    }
  ]
})

export default router
      💡defineAsyncComponent
      🖊사용법
  import { defineAsyncComponent } from 'vue'

  const AsyncComp = defineAsyncComponent(() => {
    return new Promise((resolve, reject) => {
      // ...서버에서 컴포넌트를 로드하는 로직
      resolve(/* 로드 된 컴포넌트 */)
    })
  })
  // ... 일반 컴포넌트처럼 `AsyncComp`를 사용
      🖊로딩 및 에러상태
  const AsyncComp = defineAsyncComponent({
    // 로더 함수
    loader: () => import('./Foo.vue'),

    // 비동기 컴포넌트가 로드되는 동안 사용할 로딩 컴포넌트입니다.
    loadingComponent: LoadingComponent,
    // 로딩 컴포넌트를 표시하기 전에 지연할 시간. 기본값: 200ms
    delay: 200,

    // 로드 실패 시 사용할 에러 컴포넌트
    errorComponent: ErrorComponent,
    // 시간 초과 시, 에러 컴포넌트가 표시됩니다. 기본값: 무한대
    timeout: 3000
  })

    📕 Q) 컴포넌트간 데이터전달

      💡 기본은 props + emits, 형제 간 통신은 eventBus 또는 상태 관리, 깊은 트리 구조는 provide/inject,전역 상태나 비동기 캐시 상태는 Pinia나Vuex를 활용합니다.프로젝트 규모에 따라 context 분리하거나 composition function으로 공통 로직을 재사용하기도 합니다.

    📕 API요청 관리

      💡 Axios 인스턴스를 커스터마이징해서 공통 헤더, 인터셉터, 에러 핸들링을 통일했습니다.실제 프로젝트에서는 API 요청 로직을 useApi.ts 또는 useUser.ts처럼 composable 함수로 추상화했습니다.
const useUser = () => {
  const getUser = async (id: number): Promise<User> => {
    return (await axios.get(`/api/users/${id}`)).data;
  };
  return { getUser };
};

    📕 SpringBoot API와 연동시 CORS문제 해결방법?

      💡Spring Boot 쪽에서 @CrossOrigin을 컨트롤러나 전역 설정에 추가했고,프론트엔드 쪽에서는 Vite dev server에 proxy 설정을 추가해서 로컬 환경에서도 CORS 문제 없이 연동했습니다.

    📕 [인증/인가]JWT기반 인증시 프론트에서 처리한 방법

      💡로그인 시 받은 Access Token을 localStorage에 저장하고, Axios 인터셉터를 통해 Authorization 헤더에 자동 삽입했습니다.
      💡토큰 만료 시에는 응답 코드(401)에 따라 자동으로 로그인 페이지로 리다이렉트하거나, Refresh Token 로직이 있는 경우 silent refresh 처리했습니다.

    📕 협업시 유의한점

      💡백엔드와 Swagger 문서 기반으로 스펙을 맞추고, 에러 핸들링이나 상태별 UX 처리에 신경 씁니다

  🔎 성능최적화 경험

    : 리스트 렌더링에선 v-for에 :key 명시, v-if/v-show 구분, keep-alive로 컴포넌트 캐싱 처리했습니다.이미지 lazy loading, 컴포넌트 lazy import(defineAsyncComponent)로 초기 렌더 속도 개선도 경험했습니다. Vite로 빌드 속도 개선, dev server 빠르게 구성해본 경험도 있습니다.

  🔎 테스트

    📕 1. 단위테스트

      💡 단위 테스트는 vitest + @vue/test-utils로 작성해봤고 주로 컴포넌트 동작, 이벤트 핸들링, API 요청 시점 테스트, Form 유효성 검증 등을 검증했습니다.

  🔎 상태관리

    📕 Pinia를 VueX대신사용한 이유?

      💡Pinia는 Composition API에 최적화되어 있고, store 간 의존성 관리가 쉬우며 타입 추론이 뛰어납니다.
      💡특히 비동기 로직도 store 내에 정의할 수 있어 코드 구조가 간결해졌습니다.

  🔎 Vue-Typescript

    📃경험발표: 저는 Vue 3 + TypeScript 기반으로 SPA를 개발해왔고, 백엔드는 Spring Boot와의 RESTful API 연동 경험이 있습니다.상태 관리는 주로 Pinia를 사용했고, 비동기 요청은 Axios와 composable 기반으로 구조화했으며, 전체 프로젝트는 Vite 기반으로 구성했습니다.TypeScript는 컴포넌트 타입 정의, API 응답 타입, 커스텀 훅에서 유용하게 사용하고 있습니다.

    📕 Typescript Vue에서 어떻게 활용했었는지?

      💡 props, emits, computed 값에 대한 타입 안정성 확보에 사용했습니다.
      💡 API 응답 타입은 interface로 정의하고, Axios 요청 함수에 제네릭 <T 형태로 타입 적용했습니다.
      💡 특히 Composition API로 useXXX() 훅을 만들 때, 반환 값과 파라미터 타입을 명확히 해 테스트와 재사용성을 높였습니다.

5. Typescript

  🔎 인터프리팅과정(Parser, AST)

  🔎 undefined, null차이

  🔎 interface, type차이

    📕 1. interface는 자료형자체를 선언, type은 변수안에타입에 대한 정보를 대입해서 저장

    📕 2. type은 같은이름 확장불가, 인터페이스는 가능

    📕 3. type은 number, string타입 정의가능 interface는 object만 가능

    📕 4. type은 union type, intersection type가능 interface는 불가

[union type]
type Marvel = "IronMan" | "Hulk";
type Dc = "BatMan" | "SuperMan";

[intersection type]
type FavoriteSport = "swimming" | "football";
type BallSport = "football" | "baseball";
type FavoriteBallSport = FavoriteSport & BallSport; // 'football'

  🔎 동적타입 언어에 대해

  🔎 타입스크립트사용시 좋은점

6. ReactNative

8. Next.js

 🔎 기존PagesRouter vs Next.js13에서 도입된 AppRouter

    1. pages/에 위치하여 라우트를 정의한다. 서버사이드 렌더링을 하기 위해서는 getServerSideProps라는 함수를 사용해야한다.
    2. app/에 위치하며 폴더기반 구조로 파일을 관리하고 동적 세그먼트 같은 고급 라우팅기능을 직관적으로 제공해주며 디렉토리 단위로 layout.js파일을 사용해 페이지레이아웃을 쉽게 관리 할 수 있다. fetch함수를 페이지에서 비동기로 사용하면 서버사이드 렌더링을 사용할 수 있다.

 🔎 Next.js14에서 도입된 RouteGroups기능과 활용방안

    📕 설명 : URL경로에 영향을 주지 않고 논리적인 그룹으로 페이지와 컴포넌트를 구성 할 수 있는 기능. 예를들어 app/(group1)/about/page.tsx와 app/(group2)/about/page.tsx는 동일하게 /about으로 접근 가능

 🔎 여러가지 훅

    📕 userouter

      1. 특정이벤트 발생 시 프로그래밍적으로 다른페이지 이동하거나 현재경로 정보기반으로 조건부 렌더링 할때 사용

 🔎 최적화

    📕 이미지 최적화

      1. next/image 컴포넌트를 사용해 이미지 최적화, lazy loading등을 자동으로 처리

import Image from 'next/image';

function MyComponent() {
  return (
    <div>
      <h1>My Next.js Image Optimization</h1>
      <Image
        src="/images/my-image.jpg"  // 이미지 경로
        alt="My Optimized Image"    // 이미지 alt 텍스트
        width={600}                 // 이미지 가로 크기
        height={400}                // 이미지 세로 크기
        priority                    // 페이지 로딩 시 우선적으로 로딩 (필요시 사용)
      />
    </div>
  );
}

export default MyComponent;

 🔎 렌더링 기법

    📕 SSR

    📕 SSG

    📕 ISR

    📕 CSR

 🔎 기타

    📕 동적 경로처리 방법

      1. []를 사용해서 동적경로를 정의 할 수 있다. 예를들어 pages/posts/[id].js는 /posts/1과 /posts/2 경로등을 모두 처리 할 수 있고 getStaticPaths 함수를 사용해 사전정의된 경로 목록과 데이터를 가져 올 수 있다.

8. 기타

 🔎 Webpack구조, 각종 로더의 특징

  🔎 CI/CD

  🔎 CDN

  🔎 StoryBook

  🔎 Monorepo(lerna, turborepo, yarn work space등)

    📕 특징 : 두개이상의 프로젝트가 동일 저장소에 저장되는 소프트웨어 개발전략. 프로젝트 사이에 의존성이 존재하거나 같은 제품군이거나 하는 정의된 관계가 존재해야 한다.

    📕 모노레포가 해결하는 멀티레포 문제

      1. 더쉬운 프로젝트 생성: 멀티레포 같은경우엔

저장소 생성>커미터 추가>개발환경 구축>CI/CD구축>빌드>패키지 저장소에 publish인데
모노레포에서는 저장소 생성 및 커미터 추가과정이 필요없고 개발환경,CI/CD 등도 기존DevOps를 사용하므로 새프로젝트 생성에대한 오버헤드가 없다.

      2. 더쉬운 의존성관리

      3. 단일화된 관리포인트

      4. 일관된 개발자 경험제공

  🔎 npm, pnpm, yarn, yarn2

    📕 pnpm

      1. npm과 주요차이점:

        1) 패키지 저장방식: 패키지를 전역으로 저장하고 각프로젝트의 node_modules에는 실제 파일대신 심볼릭링크를 생성
        2) 성능: 패키지 설치할때 속도가 빠르고 디스크 사용량이 적다.
        3) 일관된 의존성관리: pnpm-lock.yml파일을 통해 의존성 트리의 모든부분을 고정한다. 이를통해 모든 개발자가 동일 환경에서 동일 패키지 버전을 사용하게 만든다.
        4) workspace지원: 모노레포 프로젝트를 위한 워크스페이스 기능을 내장하고 있다. 여러 패키지를 하나의 레포지토리에서 관리할수 있으며 종속성을 효율적이게 공유할수 있다.

  🔎 브라우저에 google.com을 검색했을 때 동작과정

    1. 브라우저는 DNS서버에 google.com IP주소를 물어보고 IP주소를 갖는 서버에 요청 전송

    2. 브라우저는 HTML,CSS,JS,IMage등 렌더링 필요한 리소스요청하고 서버로부터 응답받음

    3. 서버에서 받은 데이터의 HTML을 파싱해 DOMTree를 생성

    4. style태그를 만나면 HTML파싱을 중단하고 CSS파싱해 CSSOM Tree를 생성

    5. script 태그를 만나면 HTML파싱을 중단하고 스크립트를 파싱해 AST를 생성

    6. DomTree + CSSOMTree = RenderTree생성

    7. 렌더링엔진이 RenderTree에 있는 노드를 화면에 배치

    8. RenderTree에 있는 노드의 UI를 그린다.

  🔎 Socket

    📕 1. SpringBoot + Socket.io사용 경험

      💡네, 이전 프로젝트에서 실시간 채팅 기능을 구현할 때 Socket.IO를 사용한 경험이 있습니다. 프론트엔드는 Vue3 기반 SPA였고, 백엔드는 Spring Boot + GraphQL 조합이었는데, GraphQL에서는 실시간 처리가 어려운 부분이 있어서, 실시간 메시지 전송은 Socket.IO로 보완했습니다.클라이언트 측에서는 Vue에서 소켓 연결을 전역 composable로 관리했고, 특정 이벤트(message, user-typing) 수신 시 컴포넌트 상태를 갱신했습니다.서버 측에서는 사용자 인증 정보를 토큰으로 검증하고, 특정 채널(room) 단위로 메시지를 전송하거나 브로드캐스트하는 방식으로 구성했습니다. GraphQL의 Subscriptions도 고려했지만, Spring Boot에서는 WebSocket 처리와 권한 인증이 더 유연한 Socket.IO 쪽이 맞아서 선택했습니다. 실시간 기능을 적용하면서 생긴 가장 큰 이슈는 브라우저 탭 여러 개에서 소켓 연결 중복 문제였는데,이건 socket.id로 클라이언트 식별하거나, localStorage + heartbeat 로직으로 중복 방지를 구현했습니다.
profile
UnusualFrontendDeveloper

0개의 댓글