๊ฐœ๋ฐœ ๊ณต๋ถ€/๋ฐ๋ธŒ์ฝ”์Šค TIL

[ํด๋ผ์šฐ๋”ฉ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์—”์ง€๋‹ˆ์–ด๋ง TIL] 240312, 240313 - ReactJS ๋น„๋””์˜ค ์—๋””ํ„ฐ ์ œ์ž‘ํ•˜๊ธฐ ํ”„๋กœ์ ํŠธ ๋งˆ๋ฌด๋ฆฌ...

๊ฐ€์šค์ด 2024. 3. 13. 18:13

Intro


๋ฐ๋ธŒ์ฝ”์Šค์—์„œ ์ฒซ ํ”„๋กœ์ ํŠธ๊ฐ€ ๋งˆ๋ฌด๋ฆฌ๋˜์—ˆ๋‹ค.

์ฃผ๋ง ํฌํ•จ ์ผ์ฃผ์ผ ๊ฐ„ ์ง„ํ–‰๋œ ํ”„๋กœ์ ํŠธ์˜€๋Š”๋ฐ ํ•„์ˆ˜ ๊ตฌํ˜„์‚ฌํ•ญ์€ ๋ชจ๋‘ ๊ตฌํ˜„ํ•ด์„œ ๋‹คํ–‰์ด์—ˆ๋‹ค. ํœด,,

๊ทธ๋ž˜๋„ ์š•์‹ฌ์œผ๋ก  ๋” ์‘์šฉํ•ด์„œ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ๋„ ๋„ฃ๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ๊ทธ๋Ÿฌ์ง€ ๋ชปํ•ด์„œ ์•„์‰ฌ์› ๋‹ค.

 

 

 

ํ”„๋กœ์ ํŠธ ๊ฒฐ๊ณผ


vercel๋กœ ๋ฐฐํฌ

https://project1-react-js-bootstrap-three.vercel.app/

 

Gayeon's Video Editor

 

project1-react-js-bootstrap-three.vercel.app

 

 

ํ”„๋กœ์ ํŠธ ํด๋” ๊ตฌ์กฐ

๐Ÿ“ฆsrc
 โ”ฃ ๐Ÿ“‚assets
 โ”ƒ โ”ฃ ๐Ÿ“œgithub_icon.png
 โ”ƒ โ”— ๐Ÿ“œvideo_placeholder.png
 โ”ฃ ๐Ÿ“‚components
 โ”ƒ โ”ฃ ๐Ÿ“œCustomButton.js
 โ”ƒ โ”ฃ ๐Ÿ“œFooter.js
 โ”ƒ โ”ฃ ๐Ÿ“œHeader.js
 โ”ƒ โ”ฃ ๐Ÿ“œLoadingModal.js
 โ”ƒ โ”ฃ ๐Ÿ“œMultiRangeSlider.js
 โ”ƒ โ”ฃ ๐Ÿ“œToastBox.js
 โ”ƒ โ”ฃ ๐Ÿ“œVideoConversionButton.js
 โ”ƒ โ”ฃ ๐Ÿ“œVideoEditorMain.js
 โ”ƒ โ”ฃ ๐Ÿ“œVideoPlaceholder.js
 โ”ƒ โ”ฃ ๐Ÿ“œVideoPlayer.js
 โ”ƒ โ”— ๐Ÿ“œmultiRangeSlider.css
 โ”ฃ ๐Ÿ“‚util
 โ”ƒ โ”ฃ ๐Ÿ“œreadFileAsBase64.js
 โ”ƒ โ”— ๐Ÿ“œsliderValueToVideoTime.js
 โ”ฃ ๐Ÿ“œ.DS_Store
 โ”ฃ ๐Ÿ“œApp.css
 โ”ฃ ๐Ÿ“œApp.js
 โ”ฃ ๐Ÿ“œindex.css
 โ”— ๐Ÿ“œindex.js

 

 

๋น„๋””์˜ค ๋ฐ์ดํ„ฐ ์ƒํƒœ ๊ด€๋ฆฌ

React Context API

: ๋น„๋””์˜ค ๋ฐ์ดํ„ฐ์™€ ๋น„๋””์˜ค ์ถ”๊ฐ€/์‚ญ์ œ dispatch ํ•จ์ˆ˜๋Š” Context๋กœ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์— ๊ณต๊ธ‰ํ•˜๋„๋ก ๊ตฌํ˜„

 

๋น„๋””์˜ค ์—…๋กœ๋“œ

: ‘๋น„๋””์˜ค๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”’ ์ด๋ฏธ์ง€ ํด๋ฆญ ์‹œ ๋น„๋””์˜ค ํŒŒ์ผ์„ ์—…๋กœ๋“œ

<section className={'upload-layout'}>
  <label>
    <img
      className={'video-upload-img'}
      src={video_placeholder}
      alt="๋น„๋””์˜ค๋ฅผ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”"
    />
    <input
      className={'video-upload-input'}
      type="file"
      accept="video/*"
      onChange={e => {
        onChange(e.target.files[0]);
      }}
    />
  </label>
</section>

 

 

๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ์ฝ”๋“œ

: video-react ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉํ•˜์—ฌ ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด ๊ตฌํ˜„

import React, { useContext, useEffect, useState } from 'react';
import { Player, BigPlayButton, LoadingSpinner, ControlBar } from 'video-react';
import { VideoFileContext } from '../App';

const VideoPlayer = ({ onPlayerChange, onPlayerStateChange }) => {
  const videoFile = useContext(VideoFileContext);
  const [player, setPlayer] = useState();
  const [playerState, setPlayerState] = useState();
  const [source, setSource] = useState();

  // ๋น„๋””์˜ค ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์„ค์ •
  useEffect(() => {
    setSource(URL.createObjectURL(videoFile));
  }, [videoFile]);

  // Player์˜ state๋ฅผ ๋ณ€์ˆ˜๋กœ ์ €์žฅ
  useEffect(() => {
    if (playerState) onPlayerStateChange(playerState);
  }, [playerState]);

  // Player๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๊ฒฝ์šฐ, (๋น„๋””์˜ค ๋ณ€๊ฒฝ) Player์™€ Player์˜ state ๊ฐ’ ๋ณ€๊ฒฝ
  useEffect(() => {
    onPlayerChange(player);

    if (player) {
      player.subscribeToStateChange(setPlayerState);
    }
  }, [player]);

  return (
    <div className={'video-player'}>
      <Player ref={player => setPlayer(player)} src={source} startTime={0}>
        <source src={source} />
        <BigPlayButton position="center" />
        <LoadingSpinner />
        <ControlBar disableCompletely></ControlBar>
      </Player>
    </div>
  );
};

export default VideoPlayer;

 

 

๋น„๋””์˜ค ํŽธ์ง‘ ๊ธฐ๋Šฅ

: ์•„๋ž˜์˜ ์œ ํŠœ๋ธŒ๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ Multi Range Slider ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

 

https://www.youtube.com/watch?v=pTlsnFLiK6c

 

 

 

import React, { useEffect, useRef, useState } from 'react';
import './multiRangeSlider.css';

const MultiRangeSlider = ({ onSliderChange }) => {
  const [minVal, setMinVal] = useState(0);
  const [maxVal, setMaxVal] = useState(100);
  const range = useRef(null);
  const thumbLeft = useRef(null);
  const thumbRight = useRef(null);

 
  useEffect(() => {
    // ์™ผ์ชฝ slider๊ฐ€ ์˜ค๋ฅธ์ชฝ input range์˜ ๊ฐ’ ์ด์ƒ์ผ ์ˆ˜ X
    const value = Math.min(parseInt(minVal), parseInt(maxVal) - 1);
    setMinVal(value);
    onSliderChange([minVal, maxVal]);

    if (range.current) {
      thumbLeft.current.style.left = `${value}%`;
      range.current.style.left = `${value}%`;
    }
  }, [minVal]);

  useEffect(() => {
	// ์˜ค๋ฅธ์ชฝ slider๊ฐ€ ์™ผ์ชฝ input range์˜ ๊ฐ’ ์ดํ•˜์ผ ์ˆ˜ X
    const value = Math.max(parseInt(minVal) + 1, parseInt(maxVal));
    setMaxVal(value);
    onSliderChange([minVal, maxVal]);

    if (range.current) {
      thumbRight.current.style.right = `${100 - value}%`;
      range.current.style.right = `${100 - value}%`;
    }
  }, [maxVal]);

  return (
    <div className="middle">
      <div className="multi-range-slider">
        <input
          type="range"
          min="0"
          max="100"
          value={minVal}
          onChange={e => setMinVal(e.target.value)}
        />
        <input
          type="range"
          min="0"
          max="100"
          value={maxVal}
          onChange={e => setMaxVal(e.target.value)}
        />

        <div className="slider">
          <div className="track"></div>
          <div className="range" ref={range}></div>
          <div className="thumb thumb-left" ref={thumbLeft}></div>
          <div className="thumb thumb-right" ref={thumbRight}></div>
        </div>
      </div>
    </div>
  );
};

export default MultiRangeSlider;

 

 

๋น„๋””์˜ค/GIF/์Œ์„ฑ ๋‚ด๋ณด๋‚ด๊ธฐ

: FFmpeg ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

 

 

๐Ÿ“ ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ณตํ†ต ํ•จ์ˆ˜ ๋ถ„๋ฆฌํ•˜๊ธฐ

  • writeFile
    const writeFile = async inputFileName => {
      return await ffmpeg.writeFile(
        inputFileName,
          await fetchFile(URL.createObjectURL(videoFile))
      );
    };
    
     
  • getMinMaxTime
    const getMinMaxTime = () => {
      const [min, max] = sliderValues;
      // sliderValueToVideoTime: Math.round((duration * sliderValue) / 100)
      const minTime = sliderValueToVideoTime(videoPlayerState.duration, min);
      const maxTime = sliderValueToVideoTime(videoPlayerState.duration, max);
    
      return [minTime, maxTime];
    };
    
  • downloadFile
    const downloadFile = dataURL => {
      const link = document.createElement('a');
      link.href = dataURL;
      link.setAttribute('download', '');
      link.click();
    };
    

 

ํ† ์ŠคํŠธ & ๋ชจ๋‹ฌ ๋„์šฐ๊ธฐ

  • MUI์˜ Snackbar, Modal ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„

 

 

 

 

ํ”„๋กœ์ ํŠธ ํšŒ๊ณ 


์ฒ˜์Œ์— ์›น์—์„œ CSS๋กœ ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์กฐ๋ฅผ ์žก๋Š” ๊ฒŒ ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ค์› ๋‹ค. ๐Ÿ˜ญ

์ฃผ๋กœ Flex๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ํ—ค๋”๋ฅผ ๊ณ ์ •์‹œํ‚ค๊ณ  ๋‚˜๋จธ์ง€๋ฅผ ๊ฐ€์šด๋ฐ ์ •๋ ฌํ•˜๋Š” ๋ถ€๋ถ„๋„ ๊ฝค๋‚˜ ํž˜๋“ค์—ˆ๋‹ค. ์•„์ง CSS ์‹ค๋ ฅ์ด ๋ถ€์กฑํ•œ ๊ฒƒ ๊ฐ™๋‹ค. ๊ณ„์† ํ”„๋กœ์ ํŠธ ํ•˜๋ฉด์„œ ์‹ค๋ ฅ์„ ํ‚ค์›Œ์•ผ์ง€.

 

๊ทธ๋ฆฌ๊ณ  ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„  ์ƒˆ๋กœ์šด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ๋ณด์•˜๋Š”๋ฐ video-react, FFmpeg ์ด๋Ÿฌํ•œ ๋น„๋””์˜ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๋Š” ๋ฐ์— ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ ธ๋‹ค. ๊ทธ๋ž˜๋„ ์ดํ•ดํ•˜๊ณ  ์‘์šฉ๊นŒ์ง€ ํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋ฟŒ๋“ฏํ•˜์˜€๋‹ค. ํ–ฅํ›„ ์ž‘์—…์œผ๋กœ ๋ฐฐ์† ํŽธ์ง‘ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ณ  GIF ์šฉ๋Ÿ‰์„ ๋‚ฎ์ถฐ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ณ  ์ ์šฉํ•  ์˜ˆ์ •์ด๋‹ค.