Nasza strona używa cookies. Dowiedz się więcej o celu ich używania i zmianie ustawień w przeglądarce. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Pytania rekrutacyjne dla Front-end Developera

Poznaj najczęstsze pytania rekrutacyjne dla Front-end Developera, odpowiedzi na nie i uzupełnij swoją wiedzę.

Każdy z nas w swojej karierze doświadczył rozmowy kwalifikacyjnej. W Liki Mobile Solutions część Front-end Developerów jest zaangażowana w procesy rekrutacyjne. W rezultacie, wielu z nas miało okazję brać czynny udział w rozmowie rekrutacyjnej również od tej drugiej strony, czyli z perspektywy “przesłuchującego”.

Bazując na naszym doświadczeniu, zdecydowaliśmy się podzielić pytaniami, jakie można usłyszeć, aplikując na stanowisko Front-end Developera, czy też inne stanowiska programistyczne w niejednym Software Housie. Pytania zostały przygotowane przez nasz Front-endowy team, biorący czynny udział w etapach rekrutacji, w składzie: Krzysztof Wyrzykowski, Adrian Wolański, Marcin Skrzyński oraz Michał Łyczko.

Pytania techniczne są niezbędnym elementem weryfikującym wiedzę podczas rekrutacji na stanowiska programistyczne. Przedstawiamy więc bazę pytań, z której korzystamy w Liki, wraz z przykładowymi odpowiedziami. Dotyczą one HTML-a, CSS-a,  Angulara oraz React’a. Można więc ten artykuł śmiało potraktować jako mini słownik pojęć współczesnego frontu, a także okazję do podsumowania wiedzy i odświeżenia teorii.


Angular

Jakie są różnice pomiędzy komponentami a dyrektywami?

Dyrektywa to mechanizm, za pomocą którego dołączamy zachowanie do elementów DOM, składający się z typów strukturalnych, atrybutów i komponentów.

Komponent to specyficzny rodzaj dyrektywy, który pozwala nam korzystać z funkcjonalności komponentów - hermetycznych elementów wielokrotnego użytku dostępnych w naszej aplikacji.

Warto dodać, że komponent jest jednym z typów dyrektyw. W przeciwieństwie do dyrektyw komponenty zawsze będą posiadać swój szablon. Należy też pamiętać, że tylko jeden komponent może zostać zainicjalizowany ‘per element’ w szablonie.


Czym jest NgModule?

Moduły (ngModules) są logicznymi granicami w aplikacji. Służą one do podzielenia aplikacji w celu wyodrębnienia jej funkcjonalności. Moduł angularowy przyjmuje w dekoratorze opcje takie jak:

  • imports - służące do importowania zależności w postaci modułów.
  • declarations - służy do definiowania komponentów używanych w obrębie danego modułu.
  • providers - służy do wstrzyknięcia instrukcji dla systemu DI, jaką dany dostawca ma uzyskać wartość dla danej zależności. W większości sytuacji ta tablica będzie przyjmować klasę serwisu.
  • entryComponents - tablica ta służy do deklaracji dynamicznie ładowanych komponentów, które podczas procesu kompilacji nie zostały użyte oraz skompilowane przez kompilator offline (OTC).
  • bootstrap - opcja, w której deklarujemy tzw. root czyli komponent, który zostanie osadzony w index.html.


Jak wygląda cykl życia komponentu?

Komponent w Angular ma cykl życia - szereg różnych faz, które przechodzą od “narodzin” do “śmierci”. Co więcej, możemy połączyć się z poszczególnymi fazami tak, aby uzyskać jak największą kontrolę nad naszą aplikacją. Ich wizualna reprezentacja wygląda w następujący sposób:

* constructor - gdy Angular tworzy komponent lub dyrektywę, constructor wywołuje new w klasie.

* ngOnChanges - ta metoda jest wywoływana, gdy zmienia się wartość właściwości związanej z danymi.  

* ngOnInit - następuje za każdym razem, kiedy inicjalizujemy dyrektywę / komponent.

* ngDoCheck - jest wywoływany razem z detektorem zmian danego komponentu. Pozwala nam to zaimplementować nasz własny algorytm wykrywania zmian dla danego komponentu.

Ważne: ngDoCheck i ngOnChanges nie powinny być implementowane razem na tym samym komponencie.

* ngOnDestroy - ta metoda zostanie wywołana tuż przed zniszczeniem komponentu przez Angulara.


Metody, które są obsługiwane tylko i wyłącznie przez komponenty:

* ngAfterContentInit - jest wywoływana po tym, jak Angular wykona dowolną projekcję treści do widoku komponentów.

* ngAfterContentChecked - wywoływana za każdym razem, gdy zawartość danego komponentu została sprawdzona przez mechanizm wykrywania zmian w Angularze.

* ngAfterViewInit - wywoływana, gdy widok komponentu został w pełni zainicjowany.

* ngAfterViewChecked - wywoływana za każdym razem, gdy widok danego komponentu został sprawdzony przez mechanizm wykrywania zmian w Angularze.


Typy metadanych

Metadane są używane do “ozdabiania” klasy, dzięki czemu można skonfigurować oczekiwane jej zachowanie. Metadane są reprezentowane przez tzw. dekoratory. Wyodrębniamy typy metadanych, takie jak:

  • Class decorator, np. @Component lub @NgModule.

  • Class decorator, np. @Component lub @NgModule.

  • Method decorator, np. @HostListener.

  • Parameter decorator, np. @Inject.



Guards / Resolvers

Guardy są implementowane jako serwisy, które należy zapewnić, więc z reguły tworzymy je jako klasy @Injectable. Guardy zwrócą wartość prawdziwą, jeśli użytkownik ma dostęp do route’a lub fałszywą, jeśli nie ma dostępu. Guardy mogą również zwrócić Observable lub Promise, które później przekształcają się w wartość logiczną w przypadku gdy guard nie może od razu odpowiedzieć na pytanie, na przykład może potrzebować wywołać API. Angular pozwoli użytkownikowi czekać, aż guard zwróci wartość true lub false.

Wyróżniamy 4 typy guardów:

* CanActivate - Sprawdza, czy użytkownik może odwiedzić route.

* CanActivateChild - Sprawdza, czy użytkownik może odwiedzić child route.

* CanDeactivate -  Sprawdza, czy użytkownik może opuścić route.

* CanLoad - Sprawdza, czy użytkownik może pobrać moduł, który jest załadowany z opóźnieniem (lazy-loaded).

Istnieje też specjalny typ:

* Resolve - pobiera on dane przed aktywacją route. Najczęściej używany jest do pobrania/wstrzyknięcia danych potrzebnych dla następnego route.


Zmiana strategii wykrywania zmian w komponencie

To strategia używana przez domyślny detektor do wykrywania zmian. Po ustawieniu zaczyna obowiązywać przy następnym uruchomieniu wykrywania zmiany.

Możemy ustawić dwa różne typy strategii wykrywania zmian:

* Default - domyślna strategia CheckAlways, w której wykrywanie zmian jest automatyczne, dopóki nie zostanie wyraźnie dezaktywowane.

* OnPush - strategia CheckOnce, co oznacza, że ​​automatyczne wykrywanie zmian jest dezaktywowane do czasu ponownego aktywowania poprzez ustawienie strategii na domyślne, czyli Default (CheckAlways). Wykrywanie zmian może być nadal jawnie wywoływane. Ta strategia dotyczy wszystkich dyrektyw podrzędnych i nie można ich zastąpić.


RxJS w kontekście Angulara, czyli co powinieneś wiedzieć?

W tym punkcie warto przypomnieć sobie różnicę pomiędzy Observable a Promise.  Służą one do pracy z danymi asynchronicznymi. Aczkolwiek różnice są następujące:

  • Promise zwraca nam jedną wartość (resolve) lub błąd (reject). Observable natomiast służy do pracy ze strumieniami (strumienie eventów oraz danych), które doskonale nadają się do pracy z wieloma wartościami (choć mogą też być użyte dla  pojedynczych wartości).
  • Drugą różnicą jest to, że Promise - w odróżnieniu od Observable - nie może być anulowany. Jeśli mamy jakiś oczekujący request HTTP, a dane nie są nam już potrzebne, możemy anulować to żądanie za pomocą odsubskrybowania.
  • Trzecia różnica dotyczy lazy execution. Observable w przeciwieństwie do Promise, działają w tym trybie. Oznacza to, że kod w ciele funkcji nie zostanie wykonany, dopóki nie zaczniemy nasłuchiwać - inaczej mówiąc - subskrybować.

Spójrzmy więc na poniższy przykład z Promise vs Observable oraz na console.logi, jakie zostaną zwrócone w przypadku zakomentowania then, a także Promise i subscribe na Observable. A oto przykłady.

PROMISE:

const promise = new Promise(resolve => {
	setTimeout( () => {
  	console.log('Some code in promise...');
    resolve(1);
  }, 500);
  console.log('Promise started...');
});

promise.then(val => console.log('Value from promise:', val));

console.log:
Promise started...
Some code in promise...
Value from promise: 1


Po zakomentowaniu `then()`:

const promise = new Promise(resolve => {
	setTimeout( () => {
  	console.log('Some code in promise...');
    resolve(1);
  }, 500);
  console.log('Promise started...');
});

// promise.then(val => console.log('Value from promise:', val));
console.log:
Promise started...
Some code in promise...

OBSERVABLE:

const stream$ = rxjs.Observable.create((observer) => {
	setTimeout( () => {
  	console.log('Some code in observable...');
    observer.next(1);
    observer.complete();
  }, 500);
  console.log('Observable started...');
});

stream$.subscribe(val => console.log('Value from observable:', val));
console.log:
Observable started...
Some code in observable...
Value from observable: 1


Po zakomentowaniu `subscribe()`:

const stream$ = rxjs.Observable.create((observer) => {
	setTimeout( () => {
  	console.log('Some code in observable...');
    observer.next(1);
    observer.complete();
  }, 500);
  console.log('Observable started...');
});

// stream$.subscribe(val => console.log('Value from observable:', val));
console.log:
(empty)

Preview: https://jsfiddle.net/lyczos/sr4uzybL/28/


Strumienie w Angular
to kolejne ważne zagadnienie. Z tego obszaru mogą pojawić się następujące pytania:


Kiedy korzystać z unsubscribe?

Gdy dane z jakiegoś strumienia przestają być nam potrzebne, powinniśmy pamiętać o “zabiciu” subskrypcji danego strumienia w celu uniknięcia wycieków pamięci. W Angularze są jednak sytuacje, w których wcale nie musimy martwić się manualnym zakończeniem subskrypcji. A oto lista rzeczy, którymi nie musimy się przejmować:

  • Nie musimy ‘odsubskrybować’ od observables, które się kończą (complete) lub zwrócą błąd. Aczkolwiek, nie ma w tym też nic złego! :)
  • Nie musimy przejmować się wyciekami pamięci w przypadku observables, wyscopowanych do całej  aplikacji, tj. np. subskrypcji w serwisach. Dlaczego? Services w Angularze są singletonami, co oznacza, że serwis istnieje w aplikacji tak długo, jak żyje cała aplikacja. W rezultacie nie ma możliwości, aby wystąpił wyciek pamięci.
  • Async pipe, czyli kiedy komponent jest niszczony, async sam zniszczy subskrypcje.
  • Powinniśmy korzystać z  HostListener jako sposobu na nasłuchiwanie eventów, ponieważ wtedy Angular zajmuje się usuwaniem eventListenerów za nas i zapobiega wyciekom pamięci, spowodowanym bindowaniem eventów.


Kiedy musimy o tym pamiętać?

  • Zawsze powinniśmy ‘odsubskrybować’ od observables FromGroup (form.valueChanges, from.statusChanged)
  • A także od observables Renderer2, np. renderer2.listen


W pozostałych pytaniach z tego zakresu możemy zostać poproszeni o wyjaśnienie następujących pojęć:


RxJS: Higher-Order Mapping Operators - po co i dlaczego?

Używane są do mapowania wartości na Observable. Co to oznacza i do czego może się to przydać? Wyobraźmy sobie, że chcemy przekazać do API pozycję kliknięcia (clientX). Przyjrzyjmy się dwóm przykładom: pierwszy (niepoprawny), bez użycia operatora High-order mapping, drugi z (concatMap):

click$.pipe(
  map(event => event.clientX)
).subscribe(x => {
  console.log('X', x);
  // some api call:
  api.saveClick(x).subscribe(response => {
  	// NESTED SUBSCRIPTION!!! 
  	console.log('data saved: ', response);
  });
});
click$.pipe(
  map(event => event.clientX),
  tap(x => console.log('X', x)), // debug 
  concatMap((x) => api.saveClick(x)) // high-order mapping operator
).subscribe(response => {
  console.log('data saved: ', response);
});

Wynik obu przykładów jest taki sam, jednak - jak widzimy - ten drugi jest dużo bardziej czytelny ;)


Jakie znasz  Higher-Order Mapping Operators?

concatMap, exhaustMap, switchMap, mergeMap


React

Jakie znasz wzorce projektowe w React?

HOC - High Order Component to funkcja, która przyjmuje komponent jako parametr i zwraca nowy.

Destructuring props to destrukturyzacja obiektu na pojedyncze property.

const Greeting = ({ id, name }) => {

return <div id={id}>Hello, { name }</div>

}


Function component
to najprostszy sposób na stworzenie reactowego komponentu.

function Howdy() {
 return <span>Howdy!</span> 
}


JSX spread attributes / forward props
polega na używaniu części propsów i przekazywaniu ich dalej.

const Greet = ({ name, ...restProps }) => {
 return (
  <div { …restProps }>
    Hello, { name }
  </div>
 )
}


Render props
odpowiada za przekazywanie funkcji, która będzie zawartością naszego komponentu.

// Initialization
const Greet = ({ render, ...restProps }) => {
 const name = „Jake”; 
 return render(name)
 )

// Usage
<Greet render={name => (
 <div>Hello, {name}</div>
)} />


To wszystko pozwala nam na dostanie się do danych z komponentu i wyświetlenie ich w dowolny sposób, nie zmieniając logiki tego komponentu.

Możemy też spotkać otwarte pytania, dotyczące terminów, o których musisz umieć powiedzieć kilka konstruktywnych zdań.


Opowiedz, co wiesz o HOC.

High Order Component to funkcja, która przyjmuje komponent jako parametr i zwraca nowy. Robi to, dodając własne propsy, opakowując go w wrapper lub wykonując na nim inne operacje. Z tej funkcji można intensywnie korzystać przy okazji używania ‘Reduxa’, a mianowicie, gdy za pomocą funkcji connect “wstrzykujemy” akcje oraz propsy do naszego komponentu.


Jak działa Virtual DOM i jakie widzisz jego zalety/wady. Co wiesz o React Reconciliation?

Virtual DOM to obiekt reprezentujący stan drzewa DOM. Wykorzystywany jest do obserwacji zmian w strukturze aplikacji i renderowania ich w drzewie DOM. Główną zaletą jest możliwość precyzyjnego określenia, czy zaszły jakieś zmiany w naszej aplikacji. Dzięki temu możemy wyrenderować określoną część aplikacji. Ma to wpływ na performance naszej aplikacji oraz wszystkie węzły z pozostałej jej części, zachowują swój niezmieniony stan.

Do wad można zaliczyć potrzebę ciągłej kontroli nad zachodzącymi zmianami oraz konieczność trzymania całego obiektu DOM w pamięci.

React Reconciliation to wyrenderowanie obiektu JSX w drzewie DOM. Ten złożony proces zawiera porównania, które pozwolą zdecydować, czy docelowy element może zostać użyty ponownie, czy też musimy stworzyć nowy. Dodatkowo zostają dodane również atrybuty do węzła, takie jak class, czy id oraz wyrenderowaniu tekstu danego elementu i/lub jego zagnieżdżonej struktury. Dla każdego zagnieżdżenia przeprowadzamy ten sam proces. Więcej informacji na temat reconciliation można przeczytać na oficjalnym blogu jednego z core developerów Reacta.


Jaki jest cel używania React Refs? Podaj przykład problemu, który rozwiązałeś za ich pomocą.

React Refs tworzą nam referencje do elementu. Dzięki temu mamy dostęp do pojedynczego elementu, bez potrzeby użycia `querySelector()`.

const App = () => {
 const containerRef = React.useRef(null);
 return <div ref={containerRef}>Hello</div>
}


Zastosowania mogą być różne. Najczęściej jest to manipulacja drzewem bez udziału Reacta lub praca z zewnętrzną biblioteką.


HTML / CSS / JS

Jakie są różnice między funkcją strzałkową a normalną?

W funkcji normalnej this odwołuje się do kontekstu rodzica. Natomiast funkcja strzałkowa używa tzw. lexical scoping, czyli this  odwołuje się do otaczającej przestrzeni. To jest główna i w zasadzie najważniejsza różnica. Ponadto, w funkcji strzałkowej nie możemy użyć m.in. super oraz arguments. Najłatwiejszym i zarazem najpowszechniejszym przykładem jest użycie kontekstu funkcji otaczającej, gdy przekazujemy callback:

function timer() {
  this.seconds = 0;
  
  setInterval(function addSecond() {
    this.seconds++; //błąd, funkcja addSecond() definiuje swój własny ‘this’  i nie ma dostępu do ‘this’  funkcji timer
  }, 1000);
}
function timer() {
  this.seconds = 0;
  
  setInterval(() => {
    this.seconds++; //Sukces, własność ‘this’  odnosi się do kontekstu funkcji timer
  }, 1000);
}


Na co zwracać uwagę, dbając o jak najlepszy performance animacji i transitions w CSS?

Używaj właściwości will-change. Pozwala ona “powiadomić” przeglądarkę o zmianach, jakie będziesz wprowadzać do danego elementu. Dzięki temu będzie mogła ona zoptymalizować/przydzielić zasoby, zanim właściwie uruchomimy jakąś animację. Przykładem jest np. tworzenie warstw dla transformacji 3D, zanim będziemy ich potrzebować. Skutkuje to znacznie lepszą płynnością animacji. Zakładając więc, iż będziesz chciał użyć 3D transform na jakimś elemencie, dodaj:

will-change: transform;


Stosuj ją jednak mądrze - jest bardzo ‘zasobożerna’. To oznacza, że nie powinno dodawać się jej do każdej animacji jako remedium na wszelkie problemy. Potraktuj ją raczej jako ostatnią deskę ratunku w walce z performancem ;)

Kiedy potrzeby danej animacji nie stanowią inaczej, powinno się używać tylko właściwości opacity oraz transform (position, scale, rotation). Spowodowane jest to faktem, że obie te właściwości nie wykonują operacji layoutu. Dla zauważenia różnic polecam filmik na YouTube.

I na koniec jeszcze jedna uwaga: Nie animuj wszystkiego w tym samym czasie! ;)


Wyjaśnij różnice między synchronicznym a asynchronicznym kodem

Kod synchroniczny oznacza ogólnie, że możesz wykonać tylko jedną rzecz w tym samym czasie - coś musi się skończyć, by coś innego mogło się zacząć. Program jest wykonywany linia po linii, jedna linia w tym samym czasie.

Asynchroniczny natomiast oznacza, że możesz wykonać kilka rzeczy w tym samym czasie i nie musisz czekać na ich wykonanie, aby przejść do kolejnych.


Soft

Przekaż mi instrukcje, jak przygotować Code Review, bazując na swoim doświadczeniu. Czy Twoim zdaniem warto to robić? A jeśli tak, to jakie widzisz w tym benefity?

Code Review jest złożonym procesem i w tym przypadku każdy deweloper może mieć swój własny przepis na sukces. To pytanie może być otwarciem do naprawdę ciekawej dyskusji. Oto nasze wskazówki dotyczące tego, jak powinien wyglądać proces Code Review, co również można wykorzystać do podparcia swojej opinii podczas rozmowy.

  • Code Review powinno być dyskusją między deweloperami. Nie da się zawsze zrozumieć, co ktoś miał na myśli, tak samo, jak osoba prosząca o sprawdzenie PR nie wie, co miałby w głowie programista, który będzie sprawdzał daną funkcjonalność.
  • Postaraj się przejrzeć i zrozumieć cały kod oraz zapoznaj się ze szczegółowym opisem zadania. Bez kontekstu ciężko będzie zrozumieć, co autor miał na myśli. Przeczytaj również opis do PR i sprawdź, czy przypadkiem nie pojawiły się jakieś pytania albo nie zostały podkreślone fragmenty, których deweloper jest niepewny.
  • Ważne jest, by starać się przekazywać plusy i minusy danego rozwiązania. Proponując jakieś inne, uzasadnij w paru zdaniach, dlaczego uważasz, że będzie lepsze. Ludzie potrzebują informacji, dlaczego są proszeni o wprowadzenie danej zmiany. Inaczej mogą poczuć się skrytykowani, a przecież my dążymy do tej konstruktywnej ;)
  • Unikaj sarkazmu i żartów. Nie każdy musi rozumieć Twoje poczucie humoru, co w rezultacie przynosi jedynie nieporozumienia. Lepiej tego uniknąć.
  • Odpowiadaj na wszelkie pytania/komentarze. Nie pozwól, żeby ktoś poczuł się zlekceważony.
  • Nie zwlekaj z odpowiedzią! Nie ma nic gorszego niż zmuszanie ludzi do czekania kilku dni na komentarze/mergowanie. Postępując w ten sposób, wstrzymujesz ich pracę nad kodem!
  • Dobrym pomysłem jest przygotowanie checklisty rzeczy, które zawsze powinny być zweryfikowane, np. czy testy przechodzą, czy kod jest poprawnie sformatowany, czy jest semantyczny i trzyma standardy aktualnej bazy kodu, czy wprowadzona została odpowiednia obsług błędów, czy kod spełnia regułę DRY, jak aktualne zmiany wpływają na aktualny kod.


Idealnie, gdyby większość tych punktów była zautomatyzowana.


Po co w ogóle robimy Code Reviews?

Aktualnie Code Review jest najlepszą ze znanych mi metod na zapewnienie jakości kodu i i minimalizację błędów w oprogramowaniu. Zapewnia o wdrożeniu dobrych praktyk, obowiązujących w danym projekcie, sprawdza zgodność z architekturą i ogólnie przyjętymi standardami. Nie wspominając już o olbrzymich wartościach edukacyjnych, jakie ze sobą niesie.

Oczywiście, nie da się zaprzeczyć, że posiada też wady. Przede wszystkim jest to proces potwornie czasochłonny i w pewnych sytuacjach zdarza się, że blokuje deweloperów przed kontynuowaniem pracy nad swoimi taskami. Aczkolwiek, mądrze rozplanowany i sprawnie przeprowadzony, przynosi dużo korzyści.


Podsumowanie

Mamy nadzieję, że ten artykuł przybliżył sprawy podstawowe, jak i te bardziej abstrakcyjne dla wielu deweloperów, a opisane przez nas front-endowe zagadnienia pozwolą Wam poszerzyć horyzont, a co najważniejsze -powiedzieć kilka konstruktywnych zdań podczas rozmowy rekrutacyjnej.

Pytania wybrane przez nasz team nie są przypadkowe. To nie tylko podsumowanie teorii z dziedziny Front-endu wraz z gotowymi odpowiedziami, ale również garść cennych wskazówek, które pomogą Wam zweryfikować swoją wiedzę techniczną i wskażą kierunek, w którym powinniście podążać, aby zaprezentować w pełni swoją wiedzę podczas rozmowy rekrutacyjnej.


O autorach

Adrian Wolański

To utalentowany Front-End Developer, który zawsze daje z siebie 100%. Z dużym doświadczeniem graficznym i milionem pomysłów na minutę, rozwija się w dziedzinie Front-Endu. W Liki czuwa nad projektami oraz zaraża zespół entuzjazmem jako Team Leader. Odpowie na wszystkie pytania dotyczące Angulara.

Marcin Skrzyński

W Liki rozwija skrzydła jako Front-End Developer, który ciągle poszukuje nowych tematów do zgłębienia. Żądny wiedzy, wciąż poszerza swoje kompetencje. Jako aktywny uczestnik konferencji i pasjonat technologicznych nowinek, zawsze ma coś ciekawego do powiedzenia na temat najnowszych technologii Front-Endu.

Michał Łyczko

Cechuje go profesjonalizm, sumienność i gotowość do pracy. Angular, RxJS, JavaScript - żadna z tych technologii nie jest mu obca! Jako Front-End Developer w Liki rozwiązuje wszelkie problemy dzięki swojemu technologicznemu zacięciu.

Krzysztof Wyrzykowski

To wykwalifikowany programista z bogatym doświadczeniem w tworzeniu interaktywnych aplikacji webowych i mobilnych. Od ponad 10 lat fascynuje się Front-endem. Uwielbia pracować ze wszystkimi technologiami, ale specjalizuje się w ReactJs oraz Django. W Liki od niedawna wspiera nas swoją wiedzą jako CTO. Jako zagorzały entuzjasta świata IT, w wolnym czasie chętnie podzieli się swoimi przemyśleniami w formie artykułu czy posta na blogu.

Zobacz kogo teraz szukają